Skip to content

Commit

Permalink
Added --sys-path and improved --impors, closes #26
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Jun 22, 2023
1 parent 55ba606 commit 98cfbe5
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 35 deletions.
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class PatternPortfolioView(View):

The `-s/--signatures` option will list just the signatures of the functions and classes, for example:
```bash
symbex -s -d symbex
symbex -s -f symbex/lib.py
```
<!-- [[[cog
import cog
Expand All @@ -149,18 +149,12 @@ def sorted_chunks(text):
path = pathlib.Path("symbex").resolve()
runner = CliRunner()
result = runner.invoke(cli, ["-s", "-d", str(path)])
result = runner.invoke(cli, ["-s", "-f", str(path / "lib.py")])
cog.out(
"```python\n{}\n```\n".format(sorted_chunks(result.output))
)
]]] -->
```python
# File: symbex/cli.py Line: 121
def cli(symbols, files, directories, excludes, signatures, imports, no_file, docstrings, count, silent, async_, function, class_, documented, undocumented, typed, untyped, partially_typed, fully_typed)

# File: symbex/cli.py Line: 330
def is_subpath(path: ?, parent: ?) -> bool

# File: symbex/lib.py Line: 106
def function_definition(function_node: AST)

Expand Down Expand Up @@ -200,43 +194,54 @@ def match(name: str, symbols: Iterable[str]) -> bool
<!-- [[[end]]] -->
This can be combined with other options, or you can run `symbex -s` to see every symbol in the current directory and its subdirectories.

To include estimated import paths, such as `# from symbex.lib import match`, use `--imports`:
To include estimated import paths, such as `# from symbex.lib import match`, use `--imports`. These will be calculated relative to the directory you specified, or you can pass one or more `--sys-path` options to request that imports are calculated relative to those directories as if they were on `sys.path`:

```bash
symbex --imports match
~/dev/symbex/symbex match --imports -s --sys-path ~/dev/symbex
```
Example output:
<!-- [[[cog
result = runner.invoke(cli, ["--imports", "-d", str(path), "match"])
result = runner.invoke(cli, [
"--imports", "-d", str(path), "match", "-s", "--sys-path", str(path.parent)
])
cog.out(
"```python\n{}\n```\n".format(result.stdout.strip())
)
]]] -->
```python
# File: symbex/lib.py Line: 81
# from .lib import match
# from symbex.lib import match
def match(name: str, symbols: Iterable[str]) -> bool
```
<!-- [[[end]]] -->
To suppress the `# File: ...` comments, use `--no-file` or `-n`.

So to both enable import paths and suppress File comments, use `-in` as a shortcut:
So to both show import paths and suppress File comments, use `-in` as a shortcut:
```bash
symbex -in match
```
Output:
<!-- [[[cog
result = runner.invoke(cli, [
"-in", "-d", str(path), "match", "-s", "--sys-path", str(path.parent)
])
cog.out(
"```python\n{}\n```\n".format(result.stdout.strip())
)
]]] -->
```python
# from symbex.lib import match
def match(name: str, symbols: Iterable[str]) -> bool
```
<!-- [[[end]]] -->

To include docstrings in those signatures, use `--docstrings`:
```bash
symbex --docstrings match -f symbex/lib.py
symbex match --docstrings -f symbex/lib.py
```
Example output:
<!-- [[[cog
result = runner.invoke(cli, ["--docstrings", "match", "-f", str(path / "lib.py")])
result = runner.invoke(cli, ["match", "--docstrings", "-f", str(path / "lib.py")])
cog.out(
"```python\n{}\n```\n".format(result.stdout.strip())
)
Expand Down Expand Up @@ -332,8 +337,9 @@ Options:
-d, --directory DIRECTORY Directories to search
-x, --exclude DIRECTORY Directories to exclude
-s, --signatures Show just function and class signatures
-i, --imports Show 'from x import y' lines for imported symbols
-n, --no-file Don't include the # File: comments in the output
-i, --imports Show 'from x import y' lines for imported symbols
--sys-path TEXT Calculate imports relative to these on sys.path
--docstrings Show function and class signatures plus docstrings
--count Show count of matching symbols
--silent Silently ignore Python files with parse errors
Expand Down
26 changes: 19 additions & 7 deletions symbex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,23 @@
is_flag=True,
help="Show just function and class signatures",
)
@click.option(
"-n",
"--no-file",
is_flag=True,
help="Don't include the # File: comments in the output",
)
@click.option(
"-i",
"--imports",
is_flag=True,
help="Show 'from x import y' lines for imported symbols",
)
@click.option(
"-n",
"--no-file",
is_flag=True,
help="Don't include the # File: comments in the output",
"sys_paths",
"--sys-path",
multiple=True,
help="Calculate imports relative to these on sys.path",
)
@click.option(
"--docstrings",
Expand Down Expand Up @@ -124,8 +130,9 @@ def cli(
directories,
excludes,
signatures,
imports,
no_file,
imports,
sys_paths,
docstrings,
count,
silent,
Expand Down Expand Up @@ -184,7 +191,9 @@ def cli(
# Count the number of --async functions in the project
symbex --async --count
"""
if count or docstrings or imports:
if count or docstrings:
signatures = True
if imports and not symbols:
signatures = True
# Show --help if no filter options are provided:
if not any(
Expand Down Expand Up @@ -320,7 +329,10 @@ def filter(node: ast.AST) -> bool:
bits.extend(["Line:", line_no])
print(*bits)
if imports:
print("#", import_line_for_function(node.name, path, directories))
print(
"#",
import_line_for_function(node.name, path, sys_paths or directories),
)
print(snippet)
print()
if count:
Expand Down
17 changes: 8 additions & 9 deletions symbex/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,16 @@ def import_line_for_function(
function_name: str, filepath: str, possible_root_dirs: List[str]
) -> str:
"""
Returns e.g. from foo.bar import baz if filepath is /Users/dev/foo/bar.py and function_name is baz
and possible_root_dirs is a list that contains /Users/dev
Returns eg 'from foo.bar import baz' if filepath is /Users/dev/foo/bar.py
and function_name is baz and possible_root_dirs is a list that contains
/Users/dev
"""
filepath = Path(filepath) # Convert the filepath string to a Path object
filename_without_extension = filepath.stem # Get filename without extension
filepath = Path(filepath).resolve()
filename_without_extension = filepath.stem

# Check for matches in possible_root_dirs
for root_dir in possible_root_dirs:
root_dir = Path(root_dir) # Convert the root_dir string to a Path object
root_dir = Path(root_dir).resolve()
try:
relative_path = filepath.relative_to(root_dir)
# Convert path separators to dots and assemble import line
Expand All @@ -332,10 +333,8 @@ def import_line_for_function(
)
return f"from {import_path} import {function_name}"
except ValueError:
# If the ValueError is raised, it means the filepath is not under the root_dir,
# so we just continue to the next iteration
# If ValueError is raised, the filepath is not under the root_dir
continue

# If we haven't returned by this point, none of the root_dirs matched
# So return a relative import
# If none of the root_dirs matched return a relative import
return f"from .{filename_without_extension} import {function_name}"
50 changes: 50 additions & 0 deletions tests/test_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Tests for "symbex --imports --sys-path ..."
import pathlib
import pytest
import re
from click.testing import CliRunner

from symbex.cli import cli


@pytest.fixture
def imports_dir(tmpdir):
for path, content in (
("one/foo.py", "def foo1():\n pass"),
("one/bar.py", "def bar1():\n pass"),
("two/foo.py", "def foo2():\n pass"),
("two/bar.py", "def bar2():\n pass"),
("deep/nested/three/foo.py", "def foo3():\n pass"),
("deep/nested/three/bar.py", "def bar3():\n pass"),
):
p = pathlib.Path(tmpdir / path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content, "utf-8")
return tmpdir


@pytest.mark.parametrize(
"args,sys_path,expected",
(
(["foo1"], None, "from one.foo import foo1"),
(["foo2"], None, "from two.foo import foo2"),
(["foo1"], "one/", "from foo import foo1"),
# This should force a relative import:
(["foo2"], "one/", "from .foo import foo2"),
# Various deep nested examples
(["foo3"], None, "from deep.nested.three.foo import foo3"),
(["bar3"], None, "from deep.nested.three.bar import bar3"),
(["foo3"], "deep/nested", "from three.foo import foo3"),
),
)
def test_imports(args, sys_path, expected, imports_dir):
runner = CliRunner()
args = ["-in", "-d", str(imports_dir)] + args
if sys_path:
args.extend(("--sys-path", str(imports_dir / sys_path)))
result = runner.invoke(cli, args, catch_exceptions=False)
assert result.exit_code == 0
import_line = [
line[2:] for line in result.stdout.split("\n") if line.startswith("# from")
][0]
assert import_line == expected
7 changes: 4 additions & 3 deletions tests/test_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,23 @@ def test_imports(no_file):
args = [
"func_arbitrary*",
"--imports",
"-s",
"-d",
str(pathlib.Path(__file__).parent),
] + (["--no-file"] if no_file else [])
result = runner.invoke(cli, args, catch_exceptions=False)
assert result.exit_code == 0
expected = """
# File: tests/example_symbols.py Line: 28
# from .example_symbols import func_arbitrary_positional_args
# from example_symbols import func_arbitrary_positional_args
def func_arbitrary_positional_args(*args)
# File: tests/example_symbols.py Line: 33
# from .example_symbols import func_arbitrary_keyword_args
# from example_symbols import func_arbitrary_keyword_args
def func_arbitrary_keyword_args(**kwargs)
# File: tests/example_symbols.py Line: 38
# from .example_symbols import func_arbitrary_args
# from example_symbols import func_arbitrary_args
def func_arbitrary_args(*args, **kwargs)
""".strip()
if no_file:
Expand Down

0 comments on commit 98cfbe5

Please sign in to comment.