Skip to content

Commit 3cf3067

Browse files
committed
add ImportFrom support DefinitionFinder and tests
1 parent 87e7134 commit 3cf3067

File tree

7 files changed

+86
-59
lines changed

7 files changed

+86
-59
lines changed

.vscode/settings.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"python.defaultInterpreterPath": ".venv/bin/python",
33
"python.formatting.provider": "black",
4-
"[python]": {
5-
"editor.codeActionsOnSave": {
6-
"source.organizeImports": true
7-
}
8-
},
4+
// "[python]": {
5+
// "editor.codeActionsOnSave": {
6+
// "source.organizeImports": true
7+
// }
8+
// },
99
"python.analysis.typeCheckingMode": "basic",
1010
"python.testing.pytestArgs": [
1111
"tests"

nb_autodoc/analyzers/definitionfinder.py

+25-18
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ class ClassDefData:
3838

3939

4040
@dataclass
41-
class External:
41+
class ImportFromData:
4242
order: int
43-
varname: str
43+
name: str # import asname
4444
module: str
45-
name: str
45+
orig_name: str
4646

4747

4848
def signature_from_ast(node: ast.FunctionDef) -> Signature:
@@ -67,17 +67,19 @@ class DefinitionFinder:
6767
6868
The following patterns bind names:
6969
70-
- class or function definition
71-
- assignment expression (:=)
72-
- targets that create new variable
73-
- import statement
74-
- compound statement body (if, while, for, try, with, match)
75-
- See: https://docs.python.org/3/reference/executionmodel.html#naming-and-binding
70+
- class or function definition
71+
- assignment expression (:=)
72+
- targets that create new variable
73+
- import statement
74+
- compound statement body (if, while, for, try, with, match)
75+
- See: https://docs.python.org/3/reference/executionmodel.html#naming-and-binding
76+
77+
We bind names on `from...import`, `new variable assignment`, `function or class`
7678
7779
A name can be classified into:
7880
79-
- definition: assign, function and class
80-
- external: ImportFrom matches user module
81+
- definition: assign, function and class
82+
- external: ImportFrom matches user module
8183
8284
**Variable comment:**
8385
@@ -101,11 +103,11 @@ class DefinitionFinder:
101103
will be assigned to each name.
102104
"""
103105

104-
def __init__(self, name: str, package: Optional[str]) -> None:
106+
def __init__(self, *, package: Optional[str]) -> None:
105107
# arg should all be optional (as analysis feature)
106-
self.name = name
107-
"""Module name. Determind external import."""
108-
self.package = package
108+
# self.name = name
109+
# """Module name. Determind external import."""
110+
self.package = package # maybe null
109111
"""Package name. Resolve relative import."""
110112
self.next_stmt: Optional[ast.stmt] = None
111113
self.current_classes: List[ClassDefData] = []
@@ -207,10 +209,15 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
207209
return self.visit_Assign(node) # type: ignore
208210

209211
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
212+
if self.current_function:
213+
return
210214
absoluate_module = resolve_name(node, self.package)
211-
if absoluate_module.split(".", 1)[0] == self.name.split(".", 1):
212-
for alias in node.names:
213-
varname = alias.asname or alias.name
215+
scope = self.get_current_scope()
216+
for alias in node.names:
217+
varname = alias.asname or alias.name
218+
scope[varname] = ImportFromData(
219+
next(self.counter), varname, absoluate_module, alias.name
220+
)
214221

215222
# def visit_Expr(self, node: ast.Expr) -> None:
216223
# # bound docstring on previous assign

nb_autodoc/analyzers/utils.py

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def get_assign_names(
121121
def resolve_name(
122122
name_or_import: t.Union[ast.ImportFrom, str], package: t.Optional[str] = None
123123
) -> str:
124+
"""Resolve a relative module name to an absolute one."""
124125
if isinstance(name_or_import, ast.ImportFrom):
125126
name_or_import = "." * name_or_import.level + (name_or_import.module or "")
126127
return imp_resolve_name(name_or_import, package)

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ target-version = ["py37"]
3030
profile = "black"
3131
extra_standard_library = ["typing_extensions"]
3232
skip_gitignore = true
33+
skip_glob = ["tests/analyzerdata/*"]
3334

3435
[tool.pytest.ini_options]
3536
log_cli = true # for temporary debug

tests/analyzerdata/simple-definition-ast.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
# fmt: off
33

44
import os
5-
from pathlib import Path
5+
from pathlib import Path as Path_rename
66

7-
# give module and package name for DefinitionFinder to analyze following import stmt
8-
from mypkg import ext_A, ext_fa
7+
# give package name to DefinitionFinder to analyze following import stmt
8+
from mypkg import ext_A as ext_A_rename, ext_fa
99
from mypkg.pkg import ext_B, ext_fb
1010

1111
from .util import ext_fc

tests/test_analyzers/test_definitionfinder.py

+51-32
Original file line numberDiff line numberDiff line change
@@ -6,108 +6,127 @@
66
ClassDefData,
77
DefinitionFinder,
88
FunctionDefData,
9+
ImportFromData,
910
)
1011
from nb_autodoc.analyzers.utils import ast_parse
1112

1213

1314
def get_analyzer_data(filename: str) -> str:
14-
return open(Path(__file__).parent / "analyzerdata" / filename).read()
15+
return open(Path(__file__).parent.parent / "analyzerdata" / filename).read()
1516

1617

1718
class TestDefinitionFinder:
1819
def test_simple_data(self):
1920
code = get_analyzer_data("simple-definition-ast.py")
2021
module = ast_parse(code)
21-
visitor = DefinitionFinder("mypkg.pkg", "mypkg.pkg.pkg")
22+
visitor = DefinitionFinder(package="mypkg.pkg.pkg")
2223
visitor.visit(module)
2324
del code, module
2425
module_freevars = visitor.freevars
2526
# duplicated from repr(module_freevars)
2627
# notice that AssignData.annotation is uncomparable `ast.Expression`
2728
# so until unparser implement (maybe py3.8), annotation test is unavailable
2829
assert module_freevars == {
30+
"Path_rename": ImportFromData(
31+
order=0, name="Path_rename", module="pathlib", orig_name="Path"
32+
),
33+
"ext_A_rename": ImportFromData(
34+
order=1, name="ext_A_rename", module="mypkg", orig_name="ext_A"
35+
),
36+
"ext_fa": ImportFromData(
37+
order=2, name="ext_fa", module="mypkg", orig_name="ext_fa"
38+
),
39+
"ext_B": ImportFromData(
40+
order=3, name="ext_B", module="mypkg.pkg", orig_name="ext_B"
41+
),
42+
"ext_fb": ImportFromData(
43+
order=4, name="ext_fb", module="mypkg.pkg", orig_name="ext_fb"
44+
),
45+
"ext_fc": ImportFromData(
46+
order=5, name="ext_fc", module="mypkg.pkg.pkg.util", orig_name="ext_fc"
47+
),
2948
"a": AssignData(
30-
order=0, name="a", type_comment="int", docstring="a docstring"
49+
order=6, name="a", type_comment="int", docstring="a docstring"
3150
),
32-
"a2": AssignData(order=1, name="a2", type_comment="int", docstring=None),
33-
"a3": AssignData(order=2, name="a3", type_comment=None, docstring=None),
51+
"a2": AssignData(order=7, name="a2", type_comment="int", docstring=None),
52+
"a3": AssignData(order=8, name="a3", type_comment=None, docstring=None),
3453
"b": AssignData(
35-
order=3, name="b", type_comment="int", docstring="b docstring"
54+
order=9, name="b", type_comment="int", docstring="b docstring"
3655
),
37-
"fa": FunctionDefData(order=4, name="fa"),
56+
"fa": FunctionDefData(order=10, name="fa"),
3857
"c": AssignData(
39-
order=5, name="c", type_comment=None, docstring="c and d docstring"
58+
order=11, name="c", type_comment=None, docstring="c and d docstring"
4059
),
4160
"d": AssignData(
42-
order=6, name="d", type_comment=None, docstring="c and d docstring"
61+
order=12, name="d", type_comment=None, docstring="c and d docstring"
4362
),
4463
"a1": AssignData(
45-
order=7, name="a1", type_comment=None, docstring="abcde11111 docstring"
64+
order=13, name="a1", type_comment=None, docstring="abcde11111 docstring"
4665
),
4766
"b1": AssignData(
48-
order=8, name="b1", type_comment=None, docstring="abcde11111 docstring"
67+
order=14, name="b1", type_comment=None, docstring="abcde11111 docstring"
4968
),
5069
"c1": AssignData(
51-
order=9, name="c1", type_comment=None, docstring="abcde11111 docstring"
70+
order=15, name="c1", type_comment=None, docstring="abcde11111 docstring"
5271
),
5372
"d1": AssignData(
54-
order=10, name="d1", type_comment=None, docstring="abcde11111 docstring"
73+
order=16, name="d1", type_comment=None, docstring="abcde11111 docstring"
5574
),
5675
"e1": AssignData(
57-
order=11, name="e1", type_comment=None, docstring="abcde11111 docstring"
76+
order=17, name="e1", type_comment=None, docstring="abcde11111 docstring"
5877
),
5978
"A": ClassDefData(
60-
order=12,
79+
order=18,
6180
name="A",
6281
freevars={
6382
"a": AssignData(
64-
order=13, name="a", type_comment=None, docstring=None
83+
order=19, name="a", type_comment=None, docstring=None
6584
)
6685
},
6786
instance_vars={},
6887
methods={},
6988
),
7089
"B": ClassDefData(
71-
order=14,
90+
order=20,
7291
name="B",
7392
freevars={
7493
"a": AssignData(
75-
order=15, name="a", type_comment=None, docstring="B.a docstring"
94+
order=21, name="a", type_comment=None, docstring="B.a docstring"
7695
)
7796
},
7897
instance_vars={},
7998
methods={},
8099
),
81100
"B1": ClassDefData(
82-
order=16,
101+
order=22,
83102
name="B1",
84103
freevars={
85104
"a": AssignData(
86-
order=17, name="a", type_comment=None, docstring=None
105+
order=23, name="a", type_comment=None, docstring=None
87106
),
88-
"__init__": FunctionDefData(order=18, name="__init__"),
89-
"b": FunctionDefData(order=19, name="b"),
107+
"__init__": FunctionDefData(order=24, name="__init__"),
108+
"b": FunctionDefData(order=25, name="b"),
90109
},
91110
instance_vars={},
92111
methods={},
93112
),
94113
"C": ClassDefData(
95-
order=20,
114+
order=26,
96115
name="C",
97116
freevars={
98117
"a": AssignData(
99-
order=21,
118+
order=27,
100119
name="a",
101120
type_comment=None,
102121
docstring="C.a classvar docstring",
103122
),
104-
"__init__": FunctionDefData(order=22, name="__init__"),
123+
"__init__": FunctionDefData(order=28, name="__init__"),
105124
"_A": ClassDefData(
106-
order=27,
125+
order=33,
107126
name="_A",
108127
freevars={
109128
"_a": AssignData(
110-
order=28,
129+
order=34,
111130
name="_a",
112131
type_comment=None,
113132
docstring="nested OK",
@@ -119,22 +138,22 @@ def test_simple_data(self):
119138
},
120139
instance_vars={
121140
"a": AssignData(
122-
order=23,
141+
order=29,
123142
name="a",
124143
type_comment="int | None",
125144
docstring="C instance var a/b docstring",
126145
),
127146
"b": AssignData(
128-
order=24,
147+
order=30,
129148
name="b",
130149
type_comment="int | None",
131150
docstring="C instance var a/b docstring",
132151
),
133152
"c": AssignData(
134-
order=25, name="c", type_comment=None, docstring=None
153+
order=31, name="c", type_comment=None, docstring=None
135154
),
136155
"d": AssignData(
137-
order=26,
156+
order=32,
138157
name="d",
139158
type_comment=None,
140159
docstring="C instance var d docstring",
@@ -147,7 +166,7 @@ def test_simple_data(self):
147166
def test_assigndata_override(self):
148167
code = get_analyzer_data("assigndata-override-ast.py")
149168
module = ast_parse(code)
150-
visitor = DefinitionFinder("<test>", "<test>")
169+
visitor = DefinitionFinder(package="<test>")
151170
visitor.visit(module)
152171
module_freevars = visitor.freevars
153172
assert module_freevars == {

tests/test_pkg/typing.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Dict, Optional
22

3-
43
T_foo = Dict[str, str]
54
"""typing variable T_foo
65
类型版本: 1.1.0+

0 commit comments

Comments
 (0)