Skip to content

Commit

Permalink
New options --docstrings, --documented, --undocumented - closes #25
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Jun 20, 2023
1 parent dc90c13 commit 495c377
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 24 deletions.
74 changes: 57 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ The following filters are available:
- `--function` - only functions
- `--class` - only classes
- `--async` - only `async def` functions
- `--documented` - functions/classes that have a docstring
- `--undocumented` - functions/classes that do not have a docstring
- `--typed` - functions that have at least one type annotation
- `--untyped` - functions that have no type annotations
- `--partially-typed` - functions that have some type annotations but not all
Expand Down Expand Up @@ -126,7 +128,6 @@ The `-s/--signatures` option will list just the signatures of the functions and
```bash
symbex -s -d symbex
```

<!-- [[[cog
import cog
from click.testing import CliRunner
Expand All @@ -140,43 +141,79 @@ result = runner.invoke(cli, ["-s", "-d", str(path)])
chunks = result.stdout.strip().split("\n\n")
chunks.sort()
cog.out(
"```\n{}\n```\n".format("\n\n".join(chunks))
"```python\n{}\n```\n".format("\n\n".join(chunks))
)
]]] -->
```
# File: symbex/cli.py Line: 80
def cli(symbols, files, directories, signatures, count, silent, async_, function, class_, typed, untyped, partially_typed, fully_typed)
```python
# File: symbex/cli.py Line: 95
def cli(symbols, files, directories, signatures, docstrings, count, silent, async_, function, class_, documented, undocumented, typed, untyped, partially_typed, fully_typed)

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

# File: symbex/lib.py Line: 11
# File: symbex/lib.py Line: 12
def find_symbol_nodes(code: str, filename: str, symbols: Iterable[str]) -> List[Tuple[(AST, Optional[str])]]

# File: symbex/lib.py Line: 159
# File: symbex/lib.py Line: 173
def class_definition(class_def)

# File: symbex/lib.py Line: 193
# File: symbex/lib.py Line: 207
def annotation_definition(annotation: AST) -> str

# File: symbex/lib.py Line: 211
# File: symbex/lib.py Line: 225
def read_file(path)

# File: symbex/lib.py Line: 237
# File: symbex/lib.py Line: 251
class TypeSummary

# File: symbex/lib.py Line: 242
# File: symbex/lib.py Line: 256
def type_summary(node: AST) -> Optional[TypeSummary]

# File: symbex/lib.py Line: 35
def code_for_node(code: str, node: AST, class_name: str, signatures: bool) -> Tuple[(str, int)]
# File: symbex/lib.py Line: 302
def quoted_string(s)

# File: symbex/lib.py Line: 66
def match(name: str, symbols: Iterable[str]) -> bool
# File: symbex/lib.py Line: 36
def code_for_node(code: str, node: AST, class_name: str, signatures: bool, docstrings: bool) -> Tuple[(str, int)]

# File: symbex/lib.py Line: 91
def function_definition(function_node: AST)
# File: symbex/lib.py Line: 70
def add_docstring(definition: str, node: AST, docstrings: bool, is_method: bool) -> str

# File: symbex/lib.py Line: 80
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 docstrings in those signatures, use `--docstrings`:
```bash
symbex --docstrings --documented -f symbex/lib.py
```

<!-- [[[cog
runner = CliRunner()
result = runner.invoke(cli, ["--docstrings", "--documented", "-f", str(path / "lib.py")])
# Need a consistent sort order
chunks = result.stdout.strip().split("\n\n")
chunks.sort()
cog.out(
"```python\n{}\n```\n".format("\n\n".join(chunks))
)
]]] -->
```python
# File: symbex/lib.py Line: 12
def find_symbol_nodes(code: str, filename: str, symbols: Iterable[str]) -> List[Tuple[(AST, Optional[str])]]
"Returns ast Nodes matching symbols"

# File: symbex/lib.py Line: 36
def code_for_node(code: str, node: AST, class_name: str, signatures: bool, docstrings: bool) -> Tuple[(str, int)]
"Returns the code for a given node"

# File: symbex/lib.py Line: 80
def match(name: str, symbols: Iterable[str]) -> bool
"Returns True if name matches any of the symbols, resolving wildcards"
```
<!-- [[[end]]] -->

## Counting symbols

If you just want to count the number of functions and classes that match your filters, use the `--count` option. Here's how to count your classes:
Expand Down Expand Up @@ -261,11 +298,14 @@ Options:
-f, --file FILE Files to search
-d, --directory DIRECTORY Directories to search
-s, --signatures Show just function and class signatures
--docstrings Show function and class signatures plus docstrings
--count Show count of matching symbols
--silent Silently ignore Python files with parse errors
--async Filter async functions
--function Filter functions
--class Filter classes
--documented Filter functions with docstrings
--undocumented Filter functions without docstrings
--typed Filter functions with type annotations
--untyped Filter functions without type annotations
--partially-typed Filter functions with partial type annotations
Expand Down
46 changes: 43 additions & 3 deletions symbex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
is_flag=True,
help="Show just function and class signatures",
)
@click.option(
"--docstrings",
is_flag=True,
help="Show function and class signatures plus docstrings",
)
@click.option(
"--count",
is_flag=True,
Expand Down Expand Up @@ -57,6 +62,16 @@
is_flag=True,
help="Filter classes",
)
@click.option(
"--documented",
is_flag=True,
help="Filter functions with docstrings",
)
@click.option(
"--undocumented",
is_flag=True,
help="Filter functions without docstrings",
)
@click.option(
"--typed",
is_flag=True,
Expand All @@ -82,11 +97,14 @@ def cli(
files,
directories,
signatures,
docstrings,
count,
silent,
async_,
function,
class_,
documented,
undocumented,
typed,
untyped,
partially_typed,
Expand Down Expand Up @@ -137,7 +155,7 @@ def cli(
# Count the number of --async functions in the project
symbex --async --count
"""
if count:
if count or docstrings:
signatures = True
# Show --help if no filter options are provided:
if not any(
Expand All @@ -147,6 +165,8 @@ def cli(
async_,
function,
class_,
documented,
undocumented,
typed,
untyped,
partially_typed,
Expand All @@ -164,6 +184,8 @@ def cli(
async_,
function,
class_,
documented,
undocumented,
typed,
untyped,
partially_typed,
Expand All @@ -188,7 +210,19 @@ def filter(node: ast.AST) -> bool:
return True

# If any --filters were supplied, handle them:
if any([async_, function, class_, typed, untyped, partially_typed, fully_typed]):
if any(
[
async_,
function,
class_,
documented,
undocumented,
typed,
untyped,
partially_typed,
fully_typed,
]
):

def filter(node: ast.AST) -> bool:
# Filters must ALL match
Expand All @@ -200,6 +234,10 @@ def filter(node: ast.AST) -> bool:
return False
if class_ and not isinstance(node, ast.ClassDef):
return False
if documented and not ast.get_docstring(node):
return False
if undocumented and ast.get_docstring(node):
return False
summary = type_summary(node)
# if no summary, type filters all fail
if not summary and (typed or untyped or partially_typed or fully_typed):
Expand Down Expand Up @@ -238,7 +276,9 @@ def filter(node: ast.AST) -> bool:
else:
# else print absolute path
path = file.resolve()
snippet, line_no = code_for_node(code, node, class_name, signatures)
snippet, line_no = code_for_node(
code, node, class_name, signatures, docstrings
)
bits = ["# File:", path]
if class_name:
bits.extend(["Class:", class_name])
Expand Down
29 changes: 27 additions & 2 deletions symbex/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import dataclass
from itertools import zip_longest
import re
import textwrap
from typing import Iterable, List, Optional, Tuple


Expand Down Expand Up @@ -33,7 +34,7 @@ def find_symbol_nodes(


def code_for_node(
code: str, node: AST, class_name: str, signatures: bool
code: str, node: AST, class_name: str, signatures: bool, docstrings: bool
) -> Tuple[str, int]:
"Returns the code for a given node"
lines = code.split("\n")
Expand All @@ -44,9 +45,12 @@ def code_for_node(
definition, lineno = function_definition(node), node.lineno
if class_name:
definition = " " + definition
definition = add_docstring(definition, node, docstrings, bool(class_name))
return definition, lineno
elif isinstance(node, ClassDef):
return class_definition(node), node.lineno
definition, lineno = class_definition(node), node.lineno
definition = add_docstring(definition, node, docstrings, bool(class_name))
return definition, lineno
else:
# Not a function or class, fall back on just the line
start = node.lineno - 1
Expand All @@ -63,6 +67,16 @@ def code_for_node(
return output, start + 1


def add_docstring(definition: str, node: AST, docstrings: bool, is_method: bool) -> str:
if not docstrings:
return definition
docstring = ast.get_docstring(node)
if not docstring:
return definition
docstring = quoted_string(docstring)
return f"{definition}\n{textwrap.indent(docstring, ' ' if is_method else ' ')}"


def match(name: str, symbols: Iterable[str]) -> bool:
"Returns True if name matches any of the symbols, resolving wildcards"
if name is None:
Expand Down Expand Up @@ -283,3 +297,14 @@ def type_summary(node: AST) -> Optional[TypeSummary]:
fully=fully,
partially=partially,
)


def quoted_string(s):
if "\n" in s:
# Escape triple double quotes
s = s.replace('"""', '\\"\\"\\"')
return f'"""{s}"""'
else:
# Escape double quotes
s = s.replace('"', '\\"')
return f'"{s}"'
6 changes: 6 additions & 0 deletions tests/example_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@

# Function with no arguments
def func_no_args():
"This has a single line docstring"
pass


# Function with positional arguments
def func_positional_args(a, b, c):
"""This has a
multi-line docstring"""
pass


Expand Down Expand Up @@ -139,9 +142,12 @@ def __init__(self, a: int):
pass

def method_fully_typed(self, a: int, b: str) -> bool:
"Single line"
pass

def method_partially_typed(self, a: int, b) -> bool:
"""Multiple
lines"""
pass

def method_untyped(self, a, b):
Expand Down
32 changes: 32 additions & 0 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,38 @@
["--partially-typed", "*.*"],
["def method_partially_typed"],
),
# Documented and undocumented
(
["--documented"],
[
"def func_no_args",
"def func_positional_args",
],
),
(
["--undocumented", "func_arbitrary_*"],
[
"def func_arbitrary_positional_args",
"def func_arbitrary_keyword_args",
"def func_arbitrary_args",
],
),
(
["--documented", "*.*"],
[
"def method_fully_typed",
"def method_partially_typed",
],
),
(
["--undocumented", "*.method_*"],
[
"def method_types",
"def method_positional_only_args",
"def method_keyword_only_args",
"def method_untyped",
],
),
),
)
def test_filters(args, expected):
Expand Down
Loading

0 comments on commit 495c377

Please sign in to comment.