From 572643330993c2d1efc27da8f1a659fe52f938e9 Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Sun, 2 Aug 2026 18:26:50 -0400 Subject: [PATCH 1/7] Add view_files for viewing several files in one call --- fastcore/_modidx.py | 3 ++- fastcore/editskill.py | 4 ++-- fastcore/tools.py | 13 ++++++++++++- nbs/12_tools.ipynb | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index d380de33..a8200d0e 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -743,7 +743,8 @@ 'fastcore.tools.replace_lines': ('tools.html#replace_lines', 'fastcore/tools.py'), 'fastcore.tools.str_replace': ('tools.html#str_replace', 'fastcore/tools.py'), 'fastcore.tools.strs_replace': ('tools.html#strs_replace', 'fastcore/tools.py'), - 'fastcore.tools.view_file': ('tools.html#view_file', 'fastcore/tools.py')}, + 'fastcore.tools.view_file': ('tools.html#view_file', 'fastcore/tools.py'), + 'fastcore.tools.view_files': ('tools.html#view_files', 'fastcore/tools.py')}, 'fastcore.transform': {}, 'fastcore.utils': {}, 'fastcore.xdg': { 'fastcore.xdg._path_from_env': ('xdg.html#_path_from_env', 'fastcore/xdg.py'), diff --git a/fastcore/editskill.py b/fastcore/editskill.py index 86a4b46d..334c5d51 100644 --- a/fastcore/editskill.py +++ b/fastcore/editskill.py @@ -52,13 +52,13 @@ from fastcore.tools import (insert_line, str_replace, strs_replace, replace_lines, del_lines, ast_replace, file_insert_line, file_str_replace, file_strs_replace, file_replace_lines, file_del_lines, file_ast_replace, - view_file, create_file, line_hash, lnhash, lnhash_at) + view_file, view_files, create_file, line_hash, lnhash, lnhash_at) from fastcore.nbio import (read_nb, write_nb, new_nb, mk_cell, validate_nb, validate_cell, repair_nb, repair_cell, view_cell, cell_insert_line, cell_str_replace, cell_strs_replace, cell_replace_lines, cell_del_lines, cell_ast_replace, Notebook, NbCell, find_cells, summary_nb) __all__ = ['insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'ast_replace', 'file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', - 'view_file', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', + 'view_file', 'view_files', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', 'read_nb', 'write_nb', 'new_nb', 'mk_cell', 'validate_nb', 'validate_cell', 'repair_nb', 'repair_cell', 'view_cell', 'cell_insert_line', 'cell_str_replace', 'cell_strs_replace', 'cell_replace_lines', 'cell_del_lines', 'cell_ast_replace', 'Notebook', 'NbCell', 'find_cells', 'summary_nb'] diff --git a/fastcore/tools.py b/fastcore/tools.py index bbb27f49..553d2988 100644 --- a/fastcore/tools.py +++ b/fastcore/tools.py @@ -21,7 +21,7 @@ # %% auto #0 __all__ = ['file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', 'insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'line_hash', - 'lnhash', 'lnhash_at', 'view_file', 'create_file', 'file_edit', 'ast_replace'] + 'lnhash', 'lnhash_at', 'view_file', 'view_files', 'create_file', 'file_edit', 'ast_replace'] # %% ../nbs/12_tools.ipynb #578246d2 import zlib @@ -185,6 +185,17 @@ def view_file( fmt = (lambda i,l: lnhash(i,l)+l) if lnhashs else (lambda i,l: f'{i}: {l}') if nums else (lambda i,l: l) return PrettyString('\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))) +# %% ../nbs/12_tools.ipynb #aaba417a +def view_files( + *paths:str, # Paths to view (each expands `~` if needed) + start_line:int=1, # Starting line to view (applied to each file) + end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line) + nums:bool=True, # Show line numbers? + lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers? +): + "Show several files, each preceded by a `# file ` header" + return PrettyString('\n'.join(f'# file {p}\n{view_file(p, start_line, end_line, nums=nums, lnhashs=lnhashs)}' for p in paths)) + # %% ../nbs/12_tools.ipynb #424d09e1 def create_file( path:str, # Path to create (expands `~` if needed) diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index cb7b4dc0..62f545c3 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -552,6 +552,47 @@ "res" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "aaba417a", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def view_files(\n", + " *paths:str, # Paths to view (each expands `~` if needed)\n", + " start_line:int=1, # Starting line to view (applied to each file)\n", + " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line)\n", + " nums:bool=True, # Show line numbers?\n", + " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", + "):\n", + " \"Show several files, each preceded by a `# file ` header\"\n", + " return PrettyString('\\n'.join(f'# file {p}\\n{view_file(p, start_line, end_line, nums=nums, lnhashs=lnhashs)}' for p in paths))" + ] + }, + { + "cell_type": "markdown", + "id": "a6223841", + "metadata": {}, + "source": [ + "`view_files` reads several files in one call, following the `view_msgs`/`lnhashview_cells` convention: a `# file ` header before each, and any line range applied to each file separately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "325d8a6a", + "metadata": {}, + "outputs": [], + "source": [ + "p2 = Path(tmp.name)/'other.txt'\n", + "p2.write_text('one\\ntwo\\n')\n", + "res = view_files(test_path, p2, start_line=2, end_line=2)\n", + "test_eq(res, f'# file {test_path}\\n2: beta\\n# file {p2}\\n2: two')\n", + "res" + ] + }, { "cell_type": "code", "execution_count": null, From 20717a119f9e22cf8b8bc05abd034b6ba7e3594c Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Mon, 3 Aug 2026 14:13:38 -0400 Subject: [PATCH 2/7] Rework: view_file takes *args instead of adding view_files --- fastcore/_modidx.py | 4 ++-- fastcore/editskill.py | 4 ++-- fastcore/tools.py | 37 ++++++++++++++--------------- nbs/12_tools.ipynb | 55 ++++++++++++++++++------------------------- 4 files changed, 45 insertions(+), 55 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index a8200d0e..cf5fbaaf 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -732,6 +732,7 @@ 'fastcore.test.test_stdout': ('test.html#test_stdout', 'fastcore/test.py'), 'fastcore.test.test_warns': ('test.html#test_warns', 'fastcore/test.py')}, 'fastcore.tools': { 'fastcore.tools._norm_lines': ('tools.html#_norm_lines', 'fastcore/tools.py'), + 'fastcore.tools._view_file': ('tools.html#_view_file', 'fastcore/tools.py'), 'fastcore.tools.ast_replace': ('tools.html#ast_replace', 'fastcore/tools.py'), 'fastcore.tools.create_file': ('tools.html#create_file', 'fastcore/tools.py'), 'fastcore.tools.del_lines': ('tools.html#del_lines', 'fastcore/tools.py'), @@ -743,8 +744,7 @@ 'fastcore.tools.replace_lines': ('tools.html#replace_lines', 'fastcore/tools.py'), 'fastcore.tools.str_replace': ('tools.html#str_replace', 'fastcore/tools.py'), 'fastcore.tools.strs_replace': ('tools.html#strs_replace', 'fastcore/tools.py'), - 'fastcore.tools.view_file': ('tools.html#view_file', 'fastcore/tools.py'), - 'fastcore.tools.view_files': ('tools.html#view_files', 'fastcore/tools.py')}, + 'fastcore.tools.view_file': ('tools.html#view_file', 'fastcore/tools.py')}, 'fastcore.transform': {}, 'fastcore.utils': {}, 'fastcore.xdg': { 'fastcore.xdg._path_from_env': ('xdg.html#_path_from_env', 'fastcore/xdg.py'), diff --git a/fastcore/editskill.py b/fastcore/editskill.py index 334c5d51..86a4b46d 100644 --- a/fastcore/editskill.py +++ b/fastcore/editskill.py @@ -52,13 +52,13 @@ from fastcore.tools import (insert_line, str_replace, strs_replace, replace_lines, del_lines, ast_replace, file_insert_line, file_str_replace, file_strs_replace, file_replace_lines, file_del_lines, file_ast_replace, - view_file, view_files, create_file, line_hash, lnhash, lnhash_at) + view_file, create_file, line_hash, lnhash, lnhash_at) from fastcore.nbio import (read_nb, write_nb, new_nb, mk_cell, validate_nb, validate_cell, repair_nb, repair_cell, view_cell, cell_insert_line, cell_str_replace, cell_strs_replace, cell_replace_lines, cell_del_lines, cell_ast_replace, Notebook, NbCell, find_cells, summary_nb) __all__ = ['insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'ast_replace', 'file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', - 'view_file', 'view_files', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', + 'view_file', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', 'read_nb', 'write_nb', 'new_nb', 'mk_cell', 'validate_nb', 'validate_cell', 'repair_nb', 'repair_cell', 'view_cell', 'cell_insert_line', 'cell_str_replace', 'cell_strs_replace', 'cell_replace_lines', 'cell_del_lines', 'cell_ast_replace', 'Notebook', 'NbCell', 'find_cells', 'summary_nb'] diff --git a/fastcore/tools.py b/fastcore/tools.py index 553d2988..b3a58b8b 100644 --- a/fastcore/tools.py +++ b/fastcore/tools.py @@ -21,7 +21,7 @@ # %% auto #0 __all__ = ['file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', 'insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'line_hash', - 'lnhash', 'lnhash_at', 'view_file', 'view_files', 'create_file', 'file_edit', 'ast_replace'] + 'lnhash', 'lnhash_at', 'view_file', 'create_file', 'file_edit', 'ast_replace'] # %% ../nbs/12_tools.ipynb #578246d2 import zlib @@ -167,34 +167,33 @@ def lnhash_at( return lnhash(line, s[line-1]) # %% ../nbs/12_tools.ipynb #806c957b -def view_file( - path:str, # Path to view (expands `~` if needed) - start_line:int=1, # Starting line to view - end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown) - nums:bool=True, # Show line numbers? - lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers? -): - "Read file contents, optionally limited to 1-based line range" - path = Path(path).expanduser() - lines = path.read_text().splitlines() +def _view_file(path, start_line, end_line, nums, lnhashs): + lines = Path(path).expanduser().read_text().splitlines() if not lines: return '' if end_line is None: end_line = len(lines) if end_line < 0: end_line = len(lines)+end_line+1 if not (1 <= start_line <= len(lines)): return f'error: Invalid start_line {start_line}. Valid range: 1-{len(lines)}' if end_line > len(lines): end_line = len(lines) fmt = (lambda i,l: lnhash(i,l)+l) if lnhashs else (lambda i,l: f'{i}: {l}') if nums else (lambda i,l: l) - return PrettyString('\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))) + return '\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line)) -# %% ../nbs/12_tools.ipynb #aaba417a -def view_files( - *paths:str, # Paths to view (each expands `~` if needed) - start_line:int=1, # Starting line to view (applied to each file) - end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line) +def view_file( + *args:str|int, # Paths to view (each expands `~` if needed); up to two int args are `start_line`/`end_line` + start_line:int=1, # Starting line to view + end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown) nums:bool=True, # Show line numbers? lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers? ): - "Show several files, each preceded by a `# file ` header" - return PrettyString('\n'.join(f'# file {p}\n{view_file(p, start_line, end_line, nums=nums, lnhashs=lnhashs)}' for p in paths)) + "Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several" + lns = [o for o in args if isinstance(o,int)] + paths = [o for o in args if not isinstance(o,int)] + if not paths: raise TypeError("view_file() requires at least one path") + if len(lns)>2: raise TypeError(f"At most two int args (start_line, end_line), got {len(lns)}") + if lns: start_line = lns[0] + if len(lns)==2: end_line = lns[1] + res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths] + if len(res)==1: return PrettyString(res[0]) + return PrettyString('\n'.join(f'# file {p}\n{r}' for p,r in zip(paths,res))) # %% ../nbs/12_tools.ipynb #424d09e1 def create_file( diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index 62f545c3..4dcbe26c 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -459,23 +459,33 @@ "outputs": [], "source": [ "#| export\n", - "def view_file(\n", - " path:str, # Path to view (expands `~` if needed)\n", - " start_line:int=1, # Starting line to view\n", - " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown)\n", - " nums:bool=True, # Show line numbers?\n", - " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", - "):\n", - " \"Read file contents, optionally limited to 1-based line range\"\n", - " path = Path(path).expanduser()\n", - " lines = path.read_text().splitlines()\n", + "def _view_file(path, start_line, end_line, nums, lnhashs):\n", + " lines = Path(path).expanduser().read_text().splitlines()\n", " if not lines: return ''\n", " if end_line is None: end_line = len(lines)\n", " if end_line < 0: end_line = len(lines)+end_line+1\n", " if not (1 <= start_line <= len(lines)): return f'error: Invalid start_line {start_line}. Valid range: 1-{len(lines)}'\n", " if end_line > len(lines): end_line = len(lines)\n", " fmt = (lambda i,l: lnhash(i,l)+l) if lnhashs else (lambda i,l: f'{i}: {l}') if nums else (lambda i,l: l)\n", - " return PrettyString('\\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line)))" + " return '\\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))\n", + "\n", + "def view_file(\n", + " *args:str|int, # Paths to view (each expands `~` if needed); up to two int args are `start_line`/`end_line`\n", + " start_line:int=1, # Starting line to view\n", + " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown)\n", + " nums:bool=True, # Show line numbers?\n", + " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", + "):\n", + " \"Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several\"\n", + " lns = [o for o in args if isinstance(o,int)]\n", + " paths = [o for o in args if not isinstance(o,int)]\n", + " if not paths: raise TypeError(\"view_file() requires at least one path\")\n", + " if len(lns)>2: raise TypeError(f\"At most two int args (start_line, end_line), got {len(lns)}\")\n", + " if lns: start_line = lns[0]\n", + " if len(lns)==2: end_line = lns[1]\n", + " res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths]\n", + " if len(res)==1: return PrettyString(res[0])\n", + " return PrettyString('\\n'.join(f'# file {p}\\n{r}' for p,r in zip(paths,res)))" ] }, { @@ -552,31 +562,12 @@ "res" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "aaba417a", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "def view_files(\n", - " *paths:str, # Paths to view (each expands `~` if needed)\n", - " start_line:int=1, # Starting line to view (applied to each file)\n", - " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line)\n", - " nums:bool=True, # Show line numbers?\n", - " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", - "):\n", - " \"Show several files, each preceded by a `# file ` header\"\n", - " return PrettyString('\\n'.join(f'# file {p}\\n{view_file(p, start_line, end_line, nums=nums, lnhashs=lnhashs)}' for p in paths))" - ] - }, { "cell_type": "markdown", "id": "a6223841", "metadata": {}, "source": [ - "`view_files` reads several files in one call, following the `view_msgs`/`lnhashview_cells` convention: a `# file ` header before each, and any line range applied to each file separately." + "`view_file` also reads several files in one call: extra string args are more paths, each shown under a `# file ` header, with any line range applied to each file separately. Up to two int args keep working as positional `start_line`/`end_line`, and a single path renders exactly as before, with no header.\n" ] }, { @@ -588,7 +579,7 @@ "source": [ "p2 = Path(tmp.name)/'other.txt'\n", "p2.write_text('one\\ntwo\\n')\n", - "res = view_files(test_path, p2, start_line=2, end_line=2)\n", + "res = view_file(test_path, p2, 2, 2)\n", "test_eq(res, f'# file {test_path}\\n2: beta\\n# file {p2}\\n2: two')\n", "res" ] From bc1e3a2df3764cccca7dd4fbbfd8d100a7460d1d Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Mon, 3 Aug 2026 14:21:26 -0400 Subject: [PATCH 3/7] Drop int arg sniffing: start_line/end_line are keyword-only with *paths --- fastcore/tools.py | 9 ++------- nbs/12_tools.ipynb | 17 ++++++----------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/fastcore/tools.py b/fastcore/tools.py index b3a58b8b..85a8a12e 100644 --- a/fastcore/tools.py +++ b/fastcore/tools.py @@ -6,7 +6,7 @@ File tools wrap the primitives with path I/O, returning unified diffs of what changed ("none: No changes." / "error: ..." otherwise). The path is the first argument, e.g: - view_file('~/a/b.py', 3) + view_file('~/a/b.py', start_line=3) create_file('~/a/b/c.py', 'content here') file_str_replace('myfile.py', 'old_name', 'new_name') file_del_lines('myfile.py', 2, 4) @@ -178,19 +178,14 @@ def _view_file(path, start_line, end_line, nums, lnhashs): return '\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line)) def view_file( - *args:str|int, # Paths to view (each expands `~` if needed); up to two int args are `start_line`/`end_line` + *paths:str, # Paths to view (each expands `~` if needed) start_line:int=1, # Starting line to view end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown) nums:bool=True, # Show line numbers? lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers? ): "Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several" - lns = [o for o in args if isinstance(o,int)] - paths = [o for o in args if not isinstance(o,int)] if not paths: raise TypeError("view_file() requires at least one path") - if len(lns)>2: raise TypeError(f"At most two int args (start_line, end_line), got {len(lns)}") - if lns: start_line = lns[0] - if len(lns)==2: end_line = lns[1] res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths] if len(res)==1: return PrettyString(res[0]) return PrettyString('\n'.join(f'# file {p}\n{r}' for p,r in zip(paths,res))) diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index 4dcbe26c..3cfa0acd 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -442,7 +442,7 @@ "#| export\n", "File tools wrap the primitives with path I/O, returning unified diffs of what changed (\"none: No changes.\" / \"error: ...\" otherwise). The path is the first argument, e.g:\n", "\n", - " view_file('~/a/b.py', 3)\n", + " view_file('~/a/b.py', start_line=3)\n", " create_file('~/a/b/c.py', 'content here')\n", " file_str_replace('myfile.py', 'old_name', 'new_name')\n", " file_del_lines('myfile.py', 2, 4)\n", @@ -470,19 +470,14 @@ " return '\\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))\n", "\n", "def view_file(\n", - " *args:str|int, # Paths to view (each expands `~` if needed); up to two int args are `start_line`/`end_line`\n", + " *paths:str, # Paths to view (each expands `~` if needed)\n", " start_line:int=1, # Starting line to view\n", " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown)\n", " nums:bool=True, # Show line numbers?\n", " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", "):\n", " \"Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several\"\n", - " lns = [o for o in args if isinstance(o,int)]\n", - " paths = [o for o in args if not isinstance(o,int)]\n", " if not paths: raise TypeError(\"view_file() requires at least one path\")\n", - " if len(lns)>2: raise TypeError(f\"At most two int args (start_line, end_line), got {len(lns)}\")\n", - " if lns: start_line = lns[0]\n", - " if len(lns)==2: end_line = lns[1]\n", " res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths]\n", " if len(res)==1: return PrettyString(res[0])\n", " return PrettyString('\\n'.join(f'# file {p}\\n{r}' for p,r in zip(paths,res)))" @@ -534,7 +529,7 @@ } ], "source": [ - "view_file(test_path, 2, 30)" + "view_file(test_path, start_line=2, end_line=30)" ] }, { @@ -556,7 +551,7 @@ } ], "source": [ - "res = view_file(test_path, 2, 3, lnhashs=True)\n", + "res = view_file(test_path, start_line=2, end_line=3, lnhashs=True)\n", "test_eq(res.splitlines()[0], f'{lnhash_at(test_content, 2)}beta')\n", "test_eq(view_file(test_path, nums=False), test_content[:-1])\n", "res" @@ -567,7 +562,7 @@ "id": "a6223841", "metadata": {}, "source": [ - "`view_file` also reads several files in one call: extra string args are more paths, each shown under a `# file ` header, with any line range applied to each file separately. Up to two int args keep working as positional `start_line`/`end_line`, and a single path renders exactly as before, with no header.\n" + "`view_file` also reads several files in one call: extra paths are shown each under a `# file ` header, with any line range applied to each file separately. A single path renders exactly as before, with no header.\n" ] }, { @@ -579,7 +574,7 @@ "source": [ "p2 = Path(tmp.name)/'other.txt'\n", "p2.write_text('one\\ntwo\\n')\n", - "res = view_file(test_path, p2, 2, 2)\n", + "res = view_file(test_path, p2, start_line=2, end_line=2)\n", "test_eq(res, f'# file {test_path}\\n2: beta\\n# file {p2}\\n2: two')\n", "res" ] From a8f28d7cc55f4f7137ae85699559e59aebda16ca Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Mon, 3 Aug 2026 14:34:06 -0400 Subject: [PATCH 4/7] doc --- nbs/12_tools.ipynb | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index 3cfa0acd..8435bd12 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -182,7 +182,18 @@ "execution_count": null, "id": "8079ef9d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'a-b\\nb a'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "res = str_replace('keep q\\nfix q\\nkeep q', 'q', 'y', re_filter='fix')\n", "test_eq(res, 'keep q\\nfix y\\nkeep q')\n", @@ -492,7 +503,7 @@ { "data": { "text/plain": [ - "'/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/tmpaax7zfjh/test.txt'" + "'/tmp/tmpsjvautdg/test.txt'" ] }, "execution_count": null, @@ -570,7 +581,21 @@ "execution_count": null, "id": "325d8a6a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "# file /tmp/tmpsjvautdg/test.txt\n", + "2: beta\n", + "# file /tmp/tmpsjvautdg/other.txt\n", + "2: two" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "p2 = Path(tmp.name)/'other.txt'\n", "p2.write_text('one\\ntwo\\n')\n", @@ -929,7 +954,15 @@ ] } ], - "metadata": {}, + "metadata": { + "solveit": { + "default_code": true, + "mode": "learning", + "use_thinking": true, + "use_tools": false, + "ver": 2 + } + }, "nbformat": 4, "nbformat_minor": 5 } From 1885770a8f3720293719069019a536534c4cc2af Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Mon, 3 Aug 2026 14:41:45 -0400 Subject: [PATCH 5/7] view_cell takes several cell ids via *args --- fastcore/_modidx.py | 1 + fastcore/editskill.py | 2 +- fastcore/nbio.py | 33 ++++++++++++++++++++------------- nbs/13_nbio.ipynb | 35 ++++++++++++++++++++++------------- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index cf5fbaaf..37cf61a2 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -572,6 +572,7 @@ 'fastcore.nbio._split_cell': ('nbio.html#_split_cell', 'fastcore/nbio.py'), 'fastcore.nbio._split_mime': ('nbio.html#_split_mime', 'fastcore/nbio.py'), 'fastcore.nbio._unparse_dir': ('nbio.html#_unparse_dir', 'fastcore/nbio.py'), + 'fastcore.nbio._view_cell': ('nbio.html#_view_cell', 'fastcore/nbio.py'), 'fastcore.nbio.cell2xml': ('nbio.html#cell2xml', 'fastcore/nbio.py'), 'fastcore.nbio.cell_edit': ('nbio.html#cell_edit', 'fastcore/nbio.py'), 'fastcore.nbio.cells2xml': ('nbio.html#cells2xml', 'fastcore/nbio.py'), diff --git a/fastcore/editskill.py b/fastcore/editskill.py index 86a4b46d..c8aa1e2b 100644 --- a/fastcore/editskill.py +++ b/fastcore/editskill.py @@ -13,7 +13,7 @@ - An operation on a whole carrier takes the carrier as its noun: verb_carrier. `view_file`, `create_file`, `read_nb`, `write_nb`, `view_cell`, `validate_nb`; in the dialog layer `view_msg` and `view_dlg`. Coined verbs follow the same shape: `lnhashview_cell` is "lnhashview this cell". When the verb's object is instead the medium's unit, and that unit names its carrier uniquely, no prefix is needed - the unit noun is the carrier signal: `find_msgs`, `add_msg`, `del_msgs` (msgs live only in dialogs), `find_cells`, `summary_nb`'s rows (cells live only in notebooks). - An operation within a carrier already owns its noun (`insert_line`, `del_lines`, `replace_lines`, `str_replace`), so the carrier prefixes as a namespace and the op name survives intact: carrier_op, as in `file_del_lines`, `cell_del_lines`, `msg_del_lines`. The bare op names are the text-level primitives, and every carrier version keeps the identical signature after its address arguments, so each family is learned once and recognized everywhere. -The exceptions are deliberate and closed. `str_replace` keeps the name and argument order established by Anthropic's text editor tool. Instrument-named ops put the instrument first and elide their unit: `ast_replace` (the AST pattern is how the edit finds its target) and `exhash` (hash-verified line addresses travel inside its commands), carrier-prefixed like any other line-level op: `file_ast_replace`, `msg_ast_replace`, `file_exhash`, `cell_exhash`. Converters are named x2y (`nb2dict`, `cell2xml`; in aidialog, `dlg` on exactly one side of every converter), and on a held object the converter is a `to_y` method (`nb.to_dict()`). Plural marks arity: `view_cell` takes one cell, `lnhashview_cells` several, `del_msgs` many. +The exceptions are deliberate and closed. `str_replace` keeps the name and argument order established by Anthropic's text editor tool. Instrument-named ops put the instrument first and elide their unit: `ast_replace` (the AST pattern is how the edit finds its target) and `exhash` (hash-verified line addresses travel inside its commands), carrier-prefixed like any other line-level op: `file_ast_replace`, `msg_ast_replace`, `file_exhash`, `cell_exhash`. Converters are named x2y (`nb2dict`, `cell2xml`; in aidialog, `dlg` on exactly one side of every converter), and on a held object the converter is a `to_y` method (`nb.to_dict()`). A singular view takes one or more targets (`view_file`, `view_cell`); plural names remain for inherently group operations (`del_msgs` many). ## Parameters diff --git a/fastcore/nbio.py b/fastcore/nbio.py index 4b682a41..3296d9fd 100644 --- a/fastcore/nbio.py +++ b/fastcore/nbio.py @@ -315,18 +315,7 @@ def wrapper(path:str, cell_id:str, *args, **kw): __pyskill_params__ = {'replace_params': ('start_line', 'end_line', 'n_matches', 're_filter', 'invert_filter', 'use_regex')} # %% ../nbs/13_nbio.ipynb #421b2b9c -def view_cell( - path:str, # Notebook file to read - cell_id:str, # Id of the cell to view (exact, or unique prefix) - start_line:int=1, # Starting line to view - end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line) - nums:bool=True, # Show line numbers? - lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers? - incl_out:bool=False, # Append the cell's outputs in an `` block? - trunc_out:bool=True # Truncate included outputs to ~512 chars? -): - "View a cell's source, optionally limited to 1-based line range" - cell = _nb_cell(read_nb(path), cell_id) +def _view_cell(cell, start_line, end_line, nums, lnhashs, incl_out, trunc_out): lines = cell.source.splitlines() if not lines: return '' if end_line is None or end_line > len(lines): end_line = len(lines) @@ -336,7 +325,25 @@ def view_cell( res = '\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line)) if incl_out and (o := render_text(cell.get('outputs') or [])): res += f"\n\n{truncstr(o, 512) if trunc_out else o}\n" - return PrettyString(res) + return res + +def view_cell( + path:str, # Notebook file to read + *cell_ids:str, # Ids of the cells to view (each exact, or a unique prefix) + start_line:int=1, # Starting line to view + end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line) + nums:bool=True, # Show line numbers? + lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers? + incl_out:bool=False, # Append each cell's outputs in an `` block? + trunc_out:bool=True # Truncate included outputs to ~512 chars? +): + "View one or more cells' sources, optionally limited to 1-based line range; each cell follows a `# cell ` header when several" + if not cell_ids: raise TypeError("view_cell() requires at least one cell id") + nb = read_nb(path) + cells = [_nb_cell(nb, c) for c in cell_ids] + res = [_view_cell(c, start_line, end_line, nums, lnhashs, incl_out, trunc_out) for c in cells] + if len(res)==1: return PrettyString(res[0]) + return PrettyString('\n'.join(f'# cell {c.id}\n{r}' for c,r in zip(cells,res))) # %% ../nbs/13_nbio.ipynb #86453c0f def _is_text(x): diff --git a/nbs/13_nbio.ipynb b/nbs/13_nbio.ipynb index 9005b88f..310e7e89 100644 --- a/nbs/13_nbio.ipynb +++ b/nbs/13_nbio.ipynb @@ -1214,18 +1214,7 @@ "outputs": [], "source": [ "#| export\n", - "def view_cell(\n", - " path:str, # Notebook file to read\n", - " cell_id:str, # Id of the cell to view (exact, or unique prefix)\n", - " start_line:int=1, # Starting line to view\n", - " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line)\n", - " nums:bool=True, # Show line numbers?\n", - " lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers?\n", - " incl_out:bool=False, # Append the cell's outputs in an `` block?\n", - " trunc_out:bool=True # Truncate included outputs to ~512 chars?\n", - "):\n", - " \"View a cell's source, optionally limited to 1-based line range\"\n", - " cell = _nb_cell(read_nb(path), cell_id)\n", + "def _view_cell(cell, start_line, end_line, nums, lnhashs, incl_out, trunc_out):\n", " lines = cell.source.splitlines()\n", " if not lines: return ''\n", " if end_line is None or end_line > len(lines): end_line = len(lines)\n", @@ -1235,7 +1224,25 @@ " res = '\\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))\n", " if incl_out and (o := render_text(cell.get('outputs') or [])):\n", " res += f\"\\n\\n{truncstr(o, 512) if trunc_out else o}\\n\"\n", - " return PrettyString(res)" + " return res\n", + "\n", + "def view_cell(\n", + " path:str, # Notebook file to read\n", + " *cell_ids:str, # Ids of the cells to view (each exact, or a unique prefix)\n", + " start_line:int=1, # Starting line to view\n", + " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line)\n", + " nums:bool=True, # Show line numbers?\n", + " lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers?\n", + " incl_out:bool=False, # Append each cell's outputs in an `` block?\n", + " trunc_out:bool=True # Truncate included outputs to ~512 chars?\n", + "):\n", + " \"View one or more cells' sources, optionally limited to 1-based line range; each cell follows a `# cell ` header when several\"\n", + " if not cell_ids: raise TypeError(\"view_cell() requires at least one cell id\")\n", + " nb = read_nb(path)\n", + " cells = [_nb_cell(nb, c) for c in cell_ids]\n", + " res = [_view_cell(c, start_line, end_line, nums, lnhashs, incl_out, trunc_out) for c in cells]\n", + " if len(res)==1: return PrettyString(res[0])\n", + " return PrettyString('\\n'.join(f'# cell {c.id}\\n{r}' for c,r in zip(cells,res)))" ] }, { @@ -1259,6 +1266,8 @@ "test_eq(str(view_cell(tmp_nb, cid)), '1: a=1\\n2: print(a)')\n", "test_eq(str(view_cell(tmp_nb, cid, start_line=2, nums=False)), 'print(a)')\n", "test_eq(str(view_cell(tmp_nb, cid, lnhashs=True)).splitlines()[0], lnhash(1,'a=1')+'a=1')\n", + "cid2 = read_nb(tmp_nb).cells[1].id\n", + "test_eq(str(view_cell(tmp_nb, cid, cid2, end_line=1)), f'# cell {cid}\\n1: a=1\\n# cell {cid2}\\n1: # title')\n", "res = cell_str_replace(tmp_nb, cid, 'a=1', 'a=2')\n", "assert '-a=1' in str(res) and '+a=2' in str(res)\n", "test_eq(read_nb(tmp_nb).cells[0].source, 'a=2\\nprint(a)')\n", From cd3644204ce6f65593975c836e8886af66ed272d Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Mon, 3 Aug 2026 15:11:40 -0400 Subject: [PATCH 6/7] Go plural: view_file/view_cell become view_files/view_cells --- fastcore/_modidx.py | 4 ++-- fastcore/editskill.py | 12 ++++++------ fastcore/nbio.py | 8 ++++---- fastcore/tools.py | 8 ++++---- nbs/12_tools.ipynb | 16 ++++++++-------- nbs/13_nbio.ipynb | 22 +++++++++++----------- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 37cf61a2..8338c159 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -608,7 +608,7 @@ 'fastcore.nbio.unpack_frames': ('nbio.html#unpack_frames', 'fastcore/nbio.py'), 'fastcore.nbio.validate_cell': ('nbio.html#validate_cell', 'fastcore/nbio.py'), 'fastcore.nbio.validate_nb': ('nbio.html#validate_nb', 'fastcore/nbio.py'), - 'fastcore.nbio.view_cell': ('nbio.html#view_cell', 'fastcore/nbio.py'), + 'fastcore.nbio.view_cells': ('nbio.html#view_cells', 'fastcore/nbio.py'), 'fastcore.nbio.write_nb': ('nbio.html#write_nb', 'fastcore/nbio.py')}, 'fastcore.net': { 'fastcore.net.HTTP4xxClientError': ('net.html#http4xxclienterror', 'fastcore/net.py'), 'fastcore.net.HTTP5xxServerError': ('net.html#http5xxservererror', 'fastcore/net.py'), @@ -745,7 +745,7 @@ 'fastcore.tools.replace_lines': ('tools.html#replace_lines', 'fastcore/tools.py'), 'fastcore.tools.str_replace': ('tools.html#str_replace', 'fastcore/tools.py'), 'fastcore.tools.strs_replace': ('tools.html#strs_replace', 'fastcore/tools.py'), - 'fastcore.tools.view_file': ('tools.html#view_file', 'fastcore/tools.py')}, + 'fastcore.tools.view_files': ('tools.html#view_files', 'fastcore/tools.py')}, 'fastcore.transform': {}, 'fastcore.utils': {}, 'fastcore.xdg': { 'fastcore.xdg._path_from_env': ('xdg.html#_path_from_env', 'fastcore/xdg.py'), diff --git a/fastcore/editskill.py b/fastcore/editskill.py index c8aa1e2b..60ae03fd 100644 --- a/fastcore/editskill.py +++ b/fastcore/editskill.py @@ -10,10 +10,10 @@ Two name shapes cover the toolkit, and the pivot is the verb's direct object: -- An operation on a whole carrier takes the carrier as its noun: verb_carrier. `view_file`, `create_file`, `read_nb`, `write_nb`, `view_cell`, `validate_nb`; in the dialog layer `view_msg` and `view_dlg`. Coined verbs follow the same shape: `lnhashview_cell` is "lnhashview this cell". When the verb's object is instead the medium's unit, and that unit names its carrier uniquely, no prefix is needed - the unit noun is the carrier signal: `find_msgs`, `add_msg`, `del_msgs` (msgs live only in dialogs), `find_cells`, `summary_nb`'s rows (cells live only in notebooks). +- An operation on a whole carrier takes the carrier as its noun: verb_carrier. `view_files`, `create_file`, `read_nb`, `write_nb`, `view_cells`, `validate_nb`; in the dialog layer `view_msgs` and `view_dlg`. Coined verbs follow the same shape: `lnhashview_cells` is "lnhashview these cells". When the verb's object is instead the medium's unit, and that unit names its carrier uniquely, no prefix is needed - the unit noun is the carrier signal: `find_msgs`, `add_msg`, `del_msgs` (msgs live only in dialogs), `find_cells`, `summary_nb`'s rows (cells live only in notebooks). - An operation within a carrier already owns its noun (`insert_line`, `del_lines`, `replace_lines`, `str_replace`), so the carrier prefixes as a namespace and the op name survives intact: carrier_op, as in `file_del_lines`, `cell_del_lines`, `msg_del_lines`. The bare op names are the text-level primitives, and every carrier version keeps the identical signature after its address arguments, so each family is learned once and recognized everywhere. -The exceptions are deliberate and closed. `str_replace` keeps the name and argument order established by Anthropic's text editor tool. Instrument-named ops put the instrument first and elide their unit: `ast_replace` (the AST pattern is how the edit finds its target) and `exhash` (hash-verified line addresses travel inside its commands), carrier-prefixed like any other line-level op: `file_ast_replace`, `msg_ast_replace`, `file_exhash`, `cell_exhash`. Converters are named x2y (`nb2dict`, `cell2xml`; in aidialog, `dlg` on exactly one side of every converter), and on a held object the converter is a `to_y` method (`nb.to_dict()`). A singular view takes one or more targets (`view_file`, `view_cell`); plural names remain for inherently group operations (`del_msgs` many). +The exceptions are deliberate and closed. `str_replace` keeps the name and argument order established by Anthropic's text editor tool. Instrument-named ops put the instrument first and elide their unit: `ast_replace` (the AST pattern is how the edit finds its target) and `exhash` (hash-verified line addresses travel inside its commands), carrier-prefixed like any other line-level op: `file_ast_replace`, `msg_ast_replace`, `file_exhash`, `cell_exhash`. Converters are named x2y (`nb2dict`, `cell2xml`; in aidialog, `dlg` on exactly one side of every converter), and on a held object the converter is a `to_y` method (`nb.to_dict()`). Views take one or more targets, so they use the plural name whatever the count: `view_files`, `view_cells`, and the dialog layer's `view_msgs`. ## Parameters @@ -52,14 +52,14 @@ from fastcore.tools import (insert_line, str_replace, strs_replace, replace_lines, del_lines, ast_replace, file_insert_line, file_str_replace, file_strs_replace, file_replace_lines, file_del_lines, file_ast_replace, - view_file, create_file, line_hash, lnhash, lnhash_at) + view_files, create_file, line_hash, lnhash, lnhash_at) from fastcore.nbio import (read_nb, write_nb, new_nb, mk_cell, validate_nb, validate_cell, repair_nb, repair_cell, - view_cell, cell_insert_line, cell_str_replace, cell_strs_replace, cell_replace_lines, cell_del_lines, cell_ast_replace, Notebook, NbCell, find_cells, summary_nb) + view_cells, cell_insert_line, cell_str_replace, cell_strs_replace, cell_replace_lines, cell_del_lines, cell_ast_replace, Notebook, NbCell, find_cells, summary_nb) __all__ = ['insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'ast_replace', 'file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', - 'view_file', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', + 'view_files', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', 'read_nb', 'write_nb', 'new_nb', 'mk_cell', 'validate_nb', 'validate_cell', 'repair_nb', 'repair_cell', - 'view_cell', 'cell_insert_line', 'cell_str_replace', 'cell_strs_replace', 'cell_replace_lines', 'cell_del_lines', 'cell_ast_replace', 'Notebook', 'NbCell', 'find_cells', 'summary_nb'] + 'view_cells', 'cell_insert_line', 'cell_str_replace', 'cell_strs_replace', 'cell_replace_lines', 'cell_del_lines', 'cell_ast_replace', 'Notebook', 'NbCell', 'find_cells', 'summary_nb'] __pyskill_params__ = {'replace_params': ('start_line', 'end_line', 'n_matches', 're_filter', 'invert_filter', 'use_regex')} diff --git a/fastcore/nbio.py b/fastcore/nbio.py index 3296d9fd..d04711ca 100644 --- a/fastcore/nbio.py +++ b/fastcore/nbio.py @@ -1,6 +1,6 @@ """Reading and writing Jupyter notebooks -Cell tools apply `fastcore.tools`' string editing primitives to one notebook cell's source, addressed by path and cell id, mirroring that module's file tools: the same operations and parameters, with `path, cell_id` in place of `path`. Each editor (including the structural `cell_ast_replace`) returns a diff of the change, and `view_cell` shows a cell's source with optional line numbers or exhash addresses. +Cell tools apply `fastcore.tools`' string editing primitives to one notebook cell's source, addressed by path and cell id, mirroring that module's file tools: the same operations and parameters, with `path, cell_id` in place of `path`. Each editor (including the structural `cell_ast_replace`) returns a diff of the change, and `view_cells` shows one or more cells' sources with optional line numbers or exhash addresses. Naming and parameter conventions shared across the editing toolkit are documented in `fastcore.editskill`, which also re-exports this module's editing tools. @@ -11,7 +11,7 @@ # %% auto #0 __all__ = ['langs', 'cell_insert_line', 'cell_str_replace', 'cell_strs_replace', 'cell_replace_lines', 'cell_del_lines', 'cell_ast_replace', 'nb_lang', 'NbCell', 'dict2nb', 'read_nb', 'mk_cell', 'new_nb', 'first_code_ln', - 'dir_tag', 'nb2dict', 'nb2str', 'write_nb', 'cell_edit', 'view_cell', 'validate_cell', 'validate_nb', + 'dir_tag', 'nb2dict', 'nb2str', 'write_nb', 'cell_edit', 'view_cells', 'validate_cell', 'validate_nb', 'repair_cell', 'repair_nb', 'preferred_out', 'join_out', 'mk_stream', 'mk_result', 'mk_display', 'mk_error', 'concat_streams', 'preferred_msg_out', 'render_output', 'render_outputs', 'render_text', 'item2xml', 'cell2xml', 'cells2xml', 'Notebook', 'CellRow', 'CellRows', 'summary_nb', 'find_cells', 'select_cells', @@ -327,7 +327,7 @@ def _view_cell(cell, start_line, end_line, nums, lnhashs, incl_out, trunc_out): res += f"\n\n{truncstr(o, 512) if trunc_out else o}\n" return res -def view_cell( +def view_cells( path:str, # Notebook file to read *cell_ids:str, # Ids of the cells to view (each exact, or a unique prefix) start_line:int=1, # Starting line to view @@ -338,7 +338,7 @@ def view_cell( trunc_out:bool=True # Truncate included outputs to ~512 chars? ): "View one or more cells' sources, optionally limited to 1-based line range; each cell follows a `# cell ` header when several" - if not cell_ids: raise TypeError("view_cell() requires at least one cell id") + if not cell_ids: raise TypeError("view_cells() requires at least one cell id") nb = read_nb(path) cells = [_nb_cell(nb, c) for c in cell_ids] res = [_view_cell(c, start_line, end_line, nums, lnhashs, incl_out, trunc_out) for c in cells] diff --git a/fastcore/tools.py b/fastcore/tools.py index 85a8a12e..a259ffb9 100644 --- a/fastcore/tools.py +++ b/fastcore/tools.py @@ -6,7 +6,7 @@ File tools wrap the primitives with path I/O, returning unified diffs of what changed ("none: No changes." / "error: ..." otherwise). The path is the first argument, e.g: - view_file('~/a/b.py', start_line=3) + view_files('~/a/b.py', start_line=3) create_file('~/a/b/c.py', 'content here') file_str_replace('myfile.py', 'old_name', 'new_name') file_del_lines('myfile.py', 2, 4) @@ -21,7 +21,7 @@ # %% auto #0 __all__ = ['file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', 'insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'line_hash', - 'lnhash', 'lnhash_at', 'view_file', 'create_file', 'file_edit', 'ast_replace'] + 'lnhash', 'lnhash_at', 'view_files', 'create_file', 'file_edit', 'ast_replace'] # %% ../nbs/12_tools.ipynb #578246d2 import zlib @@ -177,7 +177,7 @@ def _view_file(path, start_line, end_line, nums, lnhashs): fmt = (lambda i,l: lnhash(i,l)+l) if lnhashs else (lambda i,l: f'{i}: {l}') if nums else (lambda i,l: l) return '\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line)) -def view_file( +def view_files( *paths:str, # Paths to view (each expands `~` if needed) start_line:int=1, # Starting line to view end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown) @@ -185,7 +185,7 @@ def view_file( lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers? ): "Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several" - if not paths: raise TypeError("view_file() requires at least one path") + if not paths: raise TypeError("view_files() requires at least one path") res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths] if len(res)==1: return PrettyString(res[0]) return PrettyString('\n'.join(f'# file {p}\n{r}' for p,r in zip(paths,res))) diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index 8435bd12..bbe81610 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -453,7 +453,7 @@ "#| export\n", "File tools wrap the primitives with path I/O, returning unified diffs of what changed (\"none: No changes.\" / \"error: ...\" otherwise). The path is the first argument, e.g:\n", "\n", - " view_file('~/a/b.py', start_line=3)\n", + " view_files('~/a/b.py', start_line=3)\n", " create_file('~/a/b/c.py', 'content here')\n", " file_str_replace('myfile.py', 'old_name', 'new_name')\n", " file_del_lines('myfile.py', 2, 4)\n", @@ -480,7 +480,7 @@ " fmt = (lambda i,l: lnhash(i,l)+l) if lnhashs else (lambda i,l: f'{i}: {l}') if nums else (lambda i,l: l)\n", " return '\\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))\n", "\n", - "def view_file(\n", + "def view_files(\n", " *paths:str, # Paths to view (each expands `~` if needed)\n", " start_line:int=1, # Starting line to view\n", " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown)\n", @@ -488,7 +488,7 @@ " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", "):\n", " \"Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several\"\n", - " if not paths: raise TypeError(\"view_file() requires at least one path\")\n", + " if not paths: raise TypeError(\"view_files() requires at least one path\")\n", " res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths]\n", " if len(res)==1: return PrettyString(res[0])\n", " return PrettyString('\\n'.join(f'# file {p}\\n{r}' for p,r in zip(paths,res)))" @@ -540,7 +540,7 @@ } ], "source": [ - "view_file(test_path, start_line=2, end_line=30)" + "view_files(test_path, start_line=2, end_line=30)" ] }, { @@ -562,9 +562,9 @@ } ], "source": [ - "res = view_file(test_path, start_line=2, end_line=3, lnhashs=True)\n", + "res = view_files(test_path, start_line=2, end_line=3, lnhashs=True)\n", "test_eq(res.splitlines()[0], f'{lnhash_at(test_content, 2)}beta')\n", - "test_eq(view_file(test_path, nums=False), test_content[:-1])\n", + "test_eq(view_files(test_path, nums=False), test_content[:-1])\n", "res" ] }, @@ -573,7 +573,7 @@ "id": "a6223841", "metadata": {}, "source": [ - "`view_file` also reads several files in one call: extra paths are shown each under a `# file ` header, with any line range applied to each file separately. A single path renders exactly as before, with no header.\n" + "`view_files` takes one or more paths: several files are shown each under a `# file ` header, with any line range applied to each file separately, and a single path renders bare, with no header.\n" ] }, { @@ -599,7 +599,7 @@ "source": [ "p2 = Path(tmp.name)/'other.txt'\n", "p2.write_text('one\\ntwo\\n')\n", - "res = view_file(test_path, p2, start_line=2, end_line=2)\n", + "res = view_files(test_path, p2, start_line=2, end_line=2)\n", "test_eq(res, f'# file {test_path}\\n2: beta\\n# file {p2}\\n2: two')\n", "res" ] diff --git a/nbs/13_nbio.ipynb b/nbs/13_nbio.ipynb index 310e7e89..ccb1353e 100644 --- a/nbs/13_nbio.ipynb +++ b/nbs/13_nbio.ipynb @@ -1127,7 +1127,7 @@ "metadata": {}, "source": [ "#| export\n", - "Cell tools apply `fastcore.tools`' string editing primitives to one notebook cell's source, addressed by path and cell id, mirroring that module's file tools: the same operations and parameters, with `path, cell_id` in place of `path`. Each editor (including the structural `cell_ast_replace`) returns a diff of the change, and `view_cell` shows a cell's source with optional line numbers or exhash addresses.\n", + "Cell tools apply `fastcore.tools`' string editing primitives to one notebook cell's source, addressed by path and cell id, mirroring that module's file tools: the same operations and parameters, with `path, cell_id` in place of `path`. Each editor (including the structural `cell_ast_replace`) returns a diff of the change, and `view_cells` shows one or more cells' sources with optional line numbers or exhash addresses.\n", "\n", "Naming and parameter conventions shared across the editing toolkit are documented in `fastcore.editskill`, which also re-exports this module's editing tools." ] @@ -1226,7 +1226,7 @@ " res += f\"\\n\\n{truncstr(o, 512) if trunc_out else o}\\n\"\n", " return res\n", "\n", - "def view_cell(\n", + "def view_cells(\n", " path:str, # Notebook file to read\n", " *cell_ids:str, # Ids of the cells to view (each exact, or a unique prefix)\n", " start_line:int=1, # Starting line to view\n", @@ -1237,7 +1237,7 @@ " trunc_out:bool=True # Truncate included outputs to ~512 chars?\n", "):\n", " \"View one or more cells' sources, optionally limited to 1-based line range; each cell follows a `# cell ` header when several\"\n", - " if not cell_ids: raise TypeError(\"view_cell() requires at least one cell id\")\n", + " if not cell_ids: raise TypeError(\"view_cells() requires at least one cell id\")\n", " nb = read_nb(path)\n", " cells = [_nb_cell(nb, c) for c in cell_ids]\n", " res = [_view_cell(c, start_line, end_line, nums, lnhashs, incl_out, trunc_out) for c in cells]\n", @@ -1263,11 +1263,11 @@ "tmp_nb = Path('tmp_cells.ipynb')\n", "write_nb(new_nb([mk_cell('a=1\\nprint(a)'), mk_cell('# title', 'markdown')]), tmp_nb)\n", "cid = read_nb(tmp_nb).cells[0].id\n", - "test_eq(str(view_cell(tmp_nb, cid)), '1: a=1\\n2: print(a)')\n", - "test_eq(str(view_cell(tmp_nb, cid, start_line=2, nums=False)), 'print(a)')\n", - "test_eq(str(view_cell(tmp_nb, cid, lnhashs=True)).splitlines()[0], lnhash(1,'a=1')+'a=1')\n", + "test_eq(str(view_cells(tmp_nb, cid)), '1: a=1\\n2: print(a)')\n", + "test_eq(str(view_cells(tmp_nb, cid, start_line=2, nums=False)), 'print(a)')\n", + "test_eq(str(view_cells(tmp_nb, cid, lnhashs=True)).splitlines()[0], lnhash(1,'a=1')+'a=1')\n", "cid2 = read_nb(tmp_nb).cells[1].id\n", - "test_eq(str(view_cell(tmp_nb, cid, cid2, end_line=1)), f'# cell {cid}\\n1: a=1\\n# cell {cid2}\\n1: # title')\n", + "test_eq(str(view_cells(tmp_nb, cid, cid2, end_line=1)), f'# cell {cid}\\n1: a=1\\n# cell {cid2}\\n1: # title')\n", "res = cell_str_replace(tmp_nb, cid, 'a=1', 'a=2')\n", "assert '-a=1' in str(res) and '+a=2' in str(res)\n", "test_eq(read_nb(tmp_nb).cells[0].source, 'a=2\\nprint(a)')\n", @@ -1277,7 +1277,7 @@ "cell_replace_lines(tmp_nb, cid, new_content='b=3')\n", "test_eq(read_nb(tmp_nb).cells[0].source, 'b=3\\n') # replace_lines yields a line block, so a trailing newline\n", "assert str(cell_str_replace(tmp_nb, cid, 'q', 'r')).startswith('error:')\n", - "with expect_fail(KeyError, 'no cell id'): view_cell(tmp_nb, 'zzzz')\n", + "with expect_fail(KeyError, 'no cell id'): view_cells(tmp_nb, 'zzzz')\n", "tmp_nb.unlink()" ] }, @@ -2029,7 +2029,7 @@ "id": "19c31347", "metadata": {}, "source": [ - "`view_cell` composes with output rendering: `incl_out=True` appends the rendered outputs in an `` block (`trunc_out=False` lifts the ~512-char cap)." + "`view_cells` composes with output rendering: `incl_out=True` appends the rendered outputs in an `` block (`trunc_out=False` lifts the ~512-char cap)." ] }, { @@ -2043,8 +2043,8 @@ "vouts = [dict(output_type='execute_result', metadata={}, data={'text/plain': ['1']}, execution_count=1)]\n", "write_nb(new_nb([mk_cell('a=1\\nprint(a)', outputs=vouts)]), vnb)\n", "vcid = read_nb(vnb).cells[0].id\n", - "test_eq(str(view_cell(vnb, vcid, incl_out=True)), '1: a=1\\n2: print(a)\\n\\n1\\n')\n", - "assert '' not in str(view_cell(vnb, vcid))\n", + "test_eq(str(view_cells(vnb, vcid, incl_out=True)), '1: a=1\\n2: print(a)\\n\\n1\\n')\n", + "assert '' not in str(view_cells(vnb, vcid))\n", "vnb.unlink()" ] }, From d552ac6c6c07eba4ee3e9cfa367251e12a9c65c1 Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Mon, 3 Aug 2026 15:19:39 -0400 Subject: [PATCH 7/7] Restore view_file as the Anthropic-familiar single-file form alongside view_files --- fastcore/_modidx.py | 2 +- fastcore/editskill.py | 6 ++--- fastcore/tools.py | 29 +++++++++++++-------- nbs/12_tools.ipynb | 59 ++++++++++++++++++++++++++++--------------- 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 8338c159..bbe50ac8 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -733,7 +733,6 @@ 'fastcore.test.test_stdout': ('test.html#test_stdout', 'fastcore/test.py'), 'fastcore.test.test_warns': ('test.html#test_warns', 'fastcore/test.py')}, 'fastcore.tools': { 'fastcore.tools._norm_lines': ('tools.html#_norm_lines', 'fastcore/tools.py'), - 'fastcore.tools._view_file': ('tools.html#_view_file', 'fastcore/tools.py'), 'fastcore.tools.ast_replace': ('tools.html#ast_replace', 'fastcore/tools.py'), 'fastcore.tools.create_file': ('tools.html#create_file', 'fastcore/tools.py'), 'fastcore.tools.del_lines': ('tools.html#del_lines', 'fastcore/tools.py'), @@ -745,6 +744,7 @@ 'fastcore.tools.replace_lines': ('tools.html#replace_lines', 'fastcore/tools.py'), 'fastcore.tools.str_replace': ('tools.html#str_replace', 'fastcore/tools.py'), 'fastcore.tools.strs_replace': ('tools.html#strs_replace', 'fastcore/tools.py'), + 'fastcore.tools.view_file': ('tools.html#view_file', 'fastcore/tools.py'), 'fastcore.tools.view_files': ('tools.html#view_files', 'fastcore/tools.py')}, 'fastcore.transform': {}, 'fastcore.utils': {}, diff --git a/fastcore/editskill.py b/fastcore/editskill.py index 60ae03fd..be382181 100644 --- a/fastcore/editskill.py +++ b/fastcore/editskill.py @@ -13,7 +13,7 @@ - An operation on a whole carrier takes the carrier as its noun: verb_carrier. `view_files`, `create_file`, `read_nb`, `write_nb`, `view_cells`, `validate_nb`; in the dialog layer `view_msgs` and `view_dlg`. Coined verbs follow the same shape: `lnhashview_cells` is "lnhashview these cells". When the verb's object is instead the medium's unit, and that unit names its carrier uniquely, no prefix is needed - the unit noun is the carrier signal: `find_msgs`, `add_msg`, `del_msgs` (msgs live only in dialogs), `find_cells`, `summary_nb`'s rows (cells live only in notebooks). - An operation within a carrier already owns its noun (`insert_line`, `del_lines`, `replace_lines`, `str_replace`), so the carrier prefixes as a namespace and the op name survives intact: carrier_op, as in `file_del_lines`, `cell_del_lines`, `msg_del_lines`. The bare op names are the text-level primitives, and every carrier version keeps the identical signature after its address arguments, so each family is learned once and recognized everywhere. -The exceptions are deliberate and closed. `str_replace` keeps the name and argument order established by Anthropic's text editor tool. Instrument-named ops put the instrument first and elide their unit: `ast_replace` (the AST pattern is how the edit finds its target) and `exhash` (hash-verified line addresses travel inside its commands), carrier-prefixed like any other line-level op: `file_ast_replace`, `msg_ast_replace`, `file_exhash`, `cell_exhash`. Converters are named x2y (`nb2dict`, `cell2xml`; in aidialog, `dlg` on exactly one side of every converter), and on a held object the converter is a `to_y` method (`nb.to_dict()`). Views take one or more targets, so they use the plural name whatever the count: `view_files`, `view_cells`, and the dialog layer's `view_msgs`. +The exceptions are deliberate and closed. `str_replace` keeps the name and argument order established by Anthropic's text editor tool. Instrument-named ops put the instrument first and elide their unit: `ast_replace` (the AST pattern is how the edit finds its target) and `exhash` (hash-verified line addresses travel inside its commands), carrier-prefixed like any other line-level op: `file_ast_replace`, `msg_ast_replace`, `file_exhash`, `cell_exhash`. Converters are named x2y (`nb2dict`, `cell2xml`; in aidialog, `dlg` on exactly one side of every converter), and on a held object the converter is a `to_y` method (`nb.to_dict()`). Views take one or more targets, so they use the plural name whatever the count: `view_files`, `view_cells`, and the dialog layer's `view_msgs`; `view_file` stays alongside `view_files` as the single-file form, keeping the name and positional line range established by Anthropic's text editor tool. ## Parameters @@ -52,13 +52,13 @@ from fastcore.tools import (insert_line, str_replace, strs_replace, replace_lines, del_lines, ast_replace, file_insert_line, file_str_replace, file_strs_replace, file_replace_lines, file_del_lines, file_ast_replace, - view_files, create_file, line_hash, lnhash, lnhash_at) + view_file, view_files, create_file, line_hash, lnhash, lnhash_at) from fastcore.nbio import (read_nb, write_nb, new_nb, mk_cell, validate_nb, validate_cell, repair_nb, repair_cell, view_cells, cell_insert_line, cell_str_replace, cell_strs_replace, cell_replace_lines, cell_del_lines, cell_ast_replace, Notebook, NbCell, find_cells, summary_nb) __all__ = ['insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'ast_replace', 'file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', - 'view_files', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', + 'view_file', 'view_files', 'create_file', 'line_hash', 'lnhash', 'lnhash_at', 'read_nb', 'write_nb', 'new_nb', 'mk_cell', 'validate_nb', 'validate_cell', 'repair_nb', 'repair_cell', 'view_cells', 'cell_insert_line', 'cell_str_replace', 'cell_strs_replace', 'cell_replace_lines', 'cell_del_lines', 'cell_ast_replace', 'Notebook', 'NbCell', 'find_cells', 'summary_nb'] diff --git a/fastcore/tools.py b/fastcore/tools.py index a259ffb9..4e17943e 100644 --- a/fastcore/tools.py +++ b/fastcore/tools.py @@ -6,7 +6,7 @@ File tools wrap the primitives with path I/O, returning unified diffs of what changed ("none: No changes." / "error: ..." otherwise). The path is the first argument, e.g: - view_files('~/a/b.py', start_line=3) + view_file('~/a/b.py', 3) create_file('~/a/b/c.py', 'content here') file_str_replace('myfile.py', 'old_name', 'new_name') file_del_lines('myfile.py', 2, 4) @@ -21,7 +21,7 @@ # %% auto #0 __all__ = ['file_insert_line', 'file_str_replace', 'file_strs_replace', 'file_replace_lines', 'file_del_lines', 'file_ast_replace', 'insert_line', 'str_replace', 'strs_replace', 'replace_lines', 'del_lines', 'line_hash', - 'lnhash', 'lnhash_at', 'view_files', 'create_file', 'file_edit', 'ast_replace'] + 'lnhash', 'lnhash_at', 'view_file', 'view_files', 'create_file', 'file_edit', 'ast_replace'] # %% ../nbs/12_tools.ipynb #578246d2 import zlib @@ -167,27 +167,36 @@ def lnhash_at( return lnhash(line, s[line-1]) # %% ../nbs/12_tools.ipynb #806c957b -def _view_file(path, start_line, end_line, nums, lnhashs): - lines = Path(path).expanduser().read_text().splitlines() +def view_file( + path:str, # Path to view (expands `~` if needed) + start_line:int=1, # Starting line to view + end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown) + nums:bool=True, # Show line numbers? + lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers? +): + "Read file contents, optionally limited to 1-based line range" + path = Path(path).expanduser() + lines = path.read_text().splitlines() if not lines: return '' if end_line is None: end_line = len(lines) if end_line < 0: end_line = len(lines)+end_line+1 if not (1 <= start_line <= len(lines)): return f'error: Invalid start_line {start_line}. Valid range: 1-{len(lines)}' if end_line > len(lines): end_line = len(lines) fmt = (lambda i,l: lnhash(i,l)+l) if lnhashs else (lambda i,l: f'{i}: {l}') if nums else (lambda i,l: l) - return '\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line)) + return PrettyString('\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))) +# %% ../nbs/12_tools.ipynb #3ffec660 def view_files( *paths:str, # Paths to view (each expands `~` if needed) - start_line:int=1, # Starting line to view - end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown) + start_line:int=1, # Starting line to view (applied to each file) + end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line) nums:bool=True, # Show line numbers? lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers? ): - "Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several" + "View one or more files, each after a `# file ` header when several; any line range applies to each file separately" if not paths: raise TypeError("view_files() requires at least one path") - res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths] - if len(res)==1: return PrettyString(res[0]) + res = [view_file(p, start_line, end_line, nums=nums, lnhashs=lnhashs) for p in paths] + if len(res)==1: return res[0] return PrettyString('\n'.join(f'# file {p}\n{r}' for p,r in zip(paths,res))) # %% ../nbs/12_tools.ipynb #424d09e1 diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index bbe81610..8accf905 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -453,7 +453,7 @@ "#| export\n", "File tools wrap the primitives with path I/O, returning unified diffs of what changed (\"none: No changes.\" / \"error: ...\" otherwise). The path is the first argument, e.g:\n", "\n", - " view_files('~/a/b.py', start_line=3)\n", + " view_file('~/a/b.py', 3)\n", " create_file('~/a/b/c.py', 'content here')\n", " file_str_replace('myfile.py', 'old_name', 'new_name')\n", " file_del_lines('myfile.py', 2, 4)\n", @@ -470,28 +470,23 @@ "outputs": [], "source": [ "#| export\n", - "def _view_file(path, start_line, end_line, nums, lnhashs):\n", - " lines = Path(path).expanduser().read_text().splitlines()\n", + "def view_file(\n", + " path:str, # Path to view (expands `~` if needed)\n", + " start_line:int=1, # Starting line to view\n", + " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown)\n", + " nums:bool=True, # Show line numbers?\n", + " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", + "):\n", + " \"Read file contents, optionally limited to 1-based line range\"\n", + " path = Path(path).expanduser()\n", + " lines = path.read_text().splitlines()\n", " if not lines: return ''\n", " if end_line is None: end_line = len(lines)\n", " if end_line < 0: end_line = len(lines)+end_line+1\n", " if not (1 <= start_line <= len(lines)): return f'error: Invalid start_line {start_line}. Valid range: 1-{len(lines)}'\n", " if end_line > len(lines): end_line = len(lines)\n", " fmt = (lambda i,l: lnhash(i,l)+l) if lnhashs else (lambda i,l: f'{i}: {l}') if nums else (lambda i,l: l)\n", - " return '\\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line))\n", - "\n", - "def view_files(\n", - " *paths:str, # Paths to view (each expands `~` if needed)\n", - " start_line:int=1, # Starting line to view\n", - " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line - handy when the file size is unknown)\n", - " nums:bool=True, # Show line numbers?\n", - " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", - "):\n", - " \"Read one or more files, optionally limited to 1-based line range; each file follows a `# file ` header when several\"\n", - " if not paths: raise TypeError(\"view_files() requires at least one path\")\n", - " res = [_view_file(p, start_line, end_line, nums, lnhashs) for p in paths]\n", - " if len(res)==1: return PrettyString(res[0])\n", - " return PrettyString('\\n'.join(f'# file {p}\\n{r}' for p,r in zip(paths,res)))" + " return PrettyString('\\n'.join(fmt(i,l) for i,l in enumerate(lines[start_line-1:end_line], start_line)))" ] }, { @@ -540,7 +535,7 @@ } ], "source": [ - "view_files(test_path, start_line=2, end_line=30)" + "view_file(test_path, 2, 30)" ] }, { @@ -562,18 +557,40 @@ } ], "source": [ - "res = view_files(test_path, start_line=2, end_line=3, lnhashs=True)\n", + "res = view_file(test_path, 2, 3, lnhashs=True)\n", "test_eq(res.splitlines()[0], f'{lnhash_at(test_content, 2)}beta')\n", - "test_eq(view_files(test_path, nums=False), test_content[:-1])\n", + "test_eq(view_file(test_path, nums=False), test_content[:-1])\n", "res" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ffec660", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def view_files(\n", + " *paths:str, # Paths to view (each expands `~` if needed)\n", + " start_line:int=1, # Starting line to view (applied to each file)\n", + " end_line:int=None, # End line (defaults to last line if None; may be past EOF, which clamps to the last line)\n", + " nums:bool=True, # Show line numbers?\n", + " lnhashs:bool=False # Show exhash `lineno|hash|` addresses instead of line numbers?\n", + "):\n", + " \"View one or more files, each after a `# file ` header when several; any line range applies to each file separately\"\n", + " if not paths: raise TypeError(\"view_files() requires at least one path\")\n", + " res = [view_file(p, start_line, end_line, nums=nums, lnhashs=lnhashs) for p in paths]\n", + " if len(res)==1: return res[0]\n", + " return PrettyString('\\n'.join(f'# file {p}\\n{r}' for p,r in zip(paths,res)))" + ] + }, { "cell_type": "markdown", "id": "a6223841", "metadata": {}, "source": [ - "`view_files` takes one or more paths: several files are shown each under a `# file ` header, with any line range applied to each file separately, and a single path renders bare, with no header.\n" + "`view_files` is the plural companion (`view_file` keeps the single-file name and positional line range models know from Anthropic's text editor tool): one or more paths, several files shown each under a `# file ` header, with any line range applied to each file separately, and a single path rendered bare.\n" ] }, {