Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for stub modules semantics #72

Closed
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9144da0
Refactor future_annotations into a DefUseChains() param.
tristanlatr Aug 8, 2023
aa7f6bc
Stubs: Add support for forward references in generic arguments of bas…
tristanlatr Aug 8, 2023
16b0ba3
Stubs: Add support for explicit type aliases
tristanlatr Aug 8, 2023
cda3429
fix import
tristanlatr Aug 8, 2023
58eb450
Stubs: Add support for forward references in TypeVar arguments
tristanlatr Aug 8, 2023
ac9b5c4
Stubs: The whole base expression should be processed later. This also…
tristanlatr Aug 8, 2023
cc142e1
Fix doc
tristanlatr Aug 8, 2023
f46d075
Fix SyntaxError: Non-ASCII character
tristanlatr Aug 8, 2023
4c6952a
Add import parsing analysis.
tristanlatr Sep 18, 2023
4001c7c
Improve the understanding of imports.
tristanlatr Sep 20, 2023
d43455c
Merge branch 'master' into 55-parsing-stubs
tristanlatr Sep 20, 2023
5ba1718
Update README.rst
tristanlatr Sep 20, 2023
2a1522a
We don't need to explicitely specify is_stubs=True if the filename en…
tristanlatr Sep 21, 2023
a41b095
Add a few tests
tristanlatr Sep 21, 2023
6c08481
We don't need to special case the alias nodes since we're setting a l…
tristanlatr Sep 21, 2023
56d28b1
remove annotation
tristanlatr Sep 21, 2023
c827911
Don't use keyword-only params
tristanlatr Sep 21, 2023
7767b64
Add docs
tristanlatr Sep 21, 2023
c2debd8
Move the import parsing code into the beniget.py file.
tristanlatr Sep 21, 2023
596c4dc
Remove annotation
tristanlatr Sep 21, 2023
c6d795a
remove annotation
tristanlatr Sep 21, 2023
2c9aeef
remove f-string
tristanlatr Sep 21, 2023
7e79899
remove keyword-only arguments
tristanlatr Sep 21, 2023
0fc304e
Also defer decorators.
tristanlatr Sep 21, 2023
1e29f56
Fix python implementation check for python2
tristanlatr Sep 21, 2023
09fe650
Fix missing str.isidentifier function on python2
tristanlatr Sep 21, 2023
1748ee3
Use a function to parse imports (instead of a class)
tristanlatr Nov 11, 2023
2b9bf8c
Update readme
tristanlatr Nov 13, 2023
20df120
Merge branch 'master' into 55-parsing-stubs
tristanlatr Nov 18, 2023
895edae
Fix tests
tristanlatr Nov 18, 2023
1eb0c36
Python2 is not supportted anymore, so remove dead code.
tristanlatr Nov 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 70 additions & 13 deletions beniget/beniget.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,19 @@ class DefUseChains(ast.NodeVisitor):
One instance of DefUseChains is only suitable to analyse one AST Module in it's lifecycle.
"""

def __init__(self, filename=None):
def __init__(self, filename=None, future_annotations=False, is_stub=False):
"""
- filename: str, included in error messages if specified
- future_annotations: bool, PEP 563 mode
- is_stub: bool, stub module semantics mode, implies future_annotations=True.
When the module is a stub, there is no need for quoting to do a forward reference
inside a type alias, typevar bound argument or a classdef base expression.
"""
self.chains = {}
self.locals = defaultdict(list)

self.filename = filename
self.is_stub = is_stub
tristanlatr marked this conversation as resolved.
Show resolved Hide resolved

# deep copy of builtins, to remain reentrant
self._builtins = {k: Def(v) for k, v in Builtins.items()}
Expand Down Expand Up @@ -339,7 +344,7 @@ def __init__(self, filename=None):

# attributes set in visit_Module
self.module = None
self.future_annotations = False
self.future_annotations = is_stub or future_annotations
tristanlatr marked this conversation as resolved.
Show resolved Hide resolved

#
## helpers
Expand Down Expand Up @@ -720,10 +725,21 @@ def visit_ClassDef(self, node):
dnode = self.chains.setdefault(node, Def(node))
self.locals[self._scopes[-1]].append(dnode)

for base in node.bases:
self.visit(base).add_user(dnode)
for keyword in node.keywords:
self.visit(keyword.value).add_user(dnode)
if self.is_stub:
# special treatment for base classes in stub modules
# so they can contain forward-references.
currentscopes = list(self._scopes)
for base in node.bases:
self._defered_annotations[-1].append((
base, currentscopes, lambda dbase: dbase.add_user(dnode)))
for keyword in node.keywords:
self._defered_annotations[-1].append((
keyword.value, currentscopes, lambda dkeyword: dkeyword.add_user(dnode)))
else:
for base in node.bases:
self.visit(base).add_user(dnode)
for keyword in node.keywords:
self.visit(keyword.value).add_user(dnode)
for decorator in node.decorator_list:
self.visit(decorator).add_user(dnode)

Expand Down Expand Up @@ -757,20 +773,31 @@ def visit_Assign(self, node):
self.visit(node.value)
for target in node.targets:
self.visit(target)

def visit_AnnAssign(self, node):
if node.value:
visit_value = True
if (self.is_stub and node.value and _is_typing_name(
node.annotation, 'TypeAlias')):
# support for PEP 613 - Explicit Type Aliases
# BUT an untyped global expression 'x=int' will NOT be considered a type alias.
visit_value = False
self._defered_annotations[-1].append(
(node.value, list(self._scopes),
lambda dvalue:dvalue.add_user(dtarget)))
elif node.value:
dvalue = self.visit(node.value)

if not self.future_annotations:
dannotation = self.visit(node.annotation)
else:
self._defered_annotations[-1].append(
(node.annotation, list(self._scopes),
lambda d:dtarget.add_user(d)))

dtarget = self.visit(node.target)
if not self.future_annotations:
dtarget.add_user(dannotation)
if node.value:
if node.value and visit_value:
dvalue.add_user(dtarget)

def visit_AugAssign(self, node):
Expand Down Expand Up @@ -1104,10 +1131,23 @@ def visit_Compare(self, node):
def visit_Call(self, node):
dnode = self.chains.setdefault(node, Def(node))
self.visit(node.func).add_user(dnode)
for arg in node.args:
self.visit(arg).add_user(dnode)
for kw in node.keywords:
self.visit(kw.value).add_user(dnode)
if self.is_stub and _is_typing_name(node.func, 'TypeVar'):
# In stubs, constraints and bound argument
# of TypeVar() can be forward references.
current_scopes = list(self._scopes)
for arg in node.args:
self._defered_annotations[-1].append(
(arg, current_scopes,
lambda darg:darg.add_user(dnode)))
for kw in node.keywords:
self._defered_annotations[-1].append(
(kw.value, current_scopes,
lambda dkw:dkw.add_user(dnode)))
else:
for arg in node.args:
self.visit(arg).add_user(dnode)
for kw in node.keywords:
self.visit(kw.value).add_user(dnode)
return dnode

visit_Repr = visit_Await
Expand Down Expand Up @@ -1263,6 +1303,23 @@ def _iter_arguments(args):
if args.kwarg:
yield args.kwarg

def _is_name(expr, name, modules=()):
tristanlatr marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns True if the expression matches:
- Name(id=<name>) or
- Attribue(value=Name(id=<one of modules>), attr=<name>)
"""
if isinstance(expr, ast.Name):
return expr.id==name
if isinstance(expr, ast.Attribute):
if isinstance(expr.value, ast.Name):
if expr.value.id in modules:
return expr.attr==name
return False

def _is_typing_name(expr, name):
return _is_name(expr, name, {'typing', 'typing_extensions', 't'})

def lookup_annotation_name_defs(name, heads, locals_map):
r"""
Simple identifier -> defs resolving.
Expand Down
77 changes: 74 additions & 3 deletions tests/test_chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def captured_output():


class TestDefUseChains(TestCase):
def checkChains(self, code, ref, strict=True):
def checkChains(self, code, ref, strict=True, is_stub=False):
class StrictDefUseChains(beniget.DefUseChains):
def unbound_identifier(self, name, node):
raise RuntimeError(
Expand All @@ -35,9 +35,9 @@ def unbound_identifier(self, name, node):

node = ast.parse(code)
if strict:
c = StrictDefUseChains()
c = StrictDefUseChains(is_stub=is_stub)
else:
c = beniget.DefUseChains()
c = beniget.DefUseChains(is_stub=is_stub)
c.visit(node)
self.assertEqual(c.dump_chains(node), ref)
return node, c
Expand Down Expand Up @@ -1129,6 +1129,77 @@ class A:
'A -> (A -> (), A -> ())'], # good
strict=False
)

@skipIf(sys.version_info.major < 3, "Python 3 semantics")
def test_stubs_generic_base_forward_ref(self):
code = '''
Thing = object
class _ScandirIterator(str, int, Thing[_ScandirIterator[F]], object):
...
class C(F, k=H):
...
F = H = object
'''
self.checkChains(
code,
['Thing -> (Thing -> (Subscript -> (_ScandirIterator -> (_ScandirIterator -> (Subscript -> ((#2)))))))',
'_ScandirIterator -> (_ScandirIterator -> (Subscript -> (Subscript -> ((#0)))))',
'C -> ()',
'F -> (F -> (Subscript -> (Subscript -> (_ScandirIterator -> (_ScandirIterator -> ((#2)))))), F -> (C -> ()))',
'H -> (H -> (C -> ()))'],
is_stub=True
)

self.checkChains(
code,
['Thing -> (Thing -> (Subscript -> (_ScandirIterator -> ())))',
'_ScandirIterator -> ()',
'C -> ()',
'F -> ()',
'H -> ()'],
strict=False,
)

@skipIf(sys.version_info.major < 3, "Python 3 semantics")
def test_stubs_forward_ref(self):
code = '''
from typing import TypeAlias
LiteralValue: TypeAlias = list[LiteralValue]|object
'''
self.checkChains(
code,
['TypeAlias -> (TypeAlias -> ())',
'LiteralValue -> (LiteralValue -> (Subscript -> (BinOp -> ((#0)))), TypeAlias -> ())'],
is_stub=True
)
self.checkChains(
code,
['TypeAlias -> (TypeAlias -> ())',
'LiteralValue -> (TypeAlias -> ())'],
strict=False,
)

@skipIf(sys.version_info.major < 3, "Python 3 semantics")
def test_stubs_typevar_forward_ref(self):
code = '''
from typing import TypeVar
AnyStr = TypeVar('AnyStr', F, bound=ast.AST)
import ast
F = object
'''
self.checkChains(
code,
['TypeVar -> (TypeVar -> (Call -> ()))',
'AnyStr -> ()',
'ast -> (ast -> (Attribute -> (Call -> ())))',
'F -> (F -> (Call -> ()))'],
is_stub=True
)
self.checkChains(
code,
['TypeVar -> (TypeVar -> (Call -> ()))', 'AnyStr -> ()', 'ast -> ()', 'F -> ()'],
strict=False,
)

class TestUseDefChains(TestCase):
def checkChains(self, code, ref):
Expand Down