diff options
author | robot-piglet <robot-piglet@yandex-team.com> | 2024-07-12 12:25:51 +0300 |
---|---|---|
committer | robot-piglet <robot-piglet@yandex-team.com> | 2024-07-12 12:36:25 +0300 |
commit | e2cd55ed29090e1b5eb657dd0ea39166329ad5ea (patch) | |
tree | 2faa7a162bb6ac7f5407587bf34807f439951acd | |
parent | cb140f96a0af418170e6ad7e60cb6752cf209ea8 (diff) | |
download | ydb-e2cd55ed29090e1b5eb657dd0ea39166329ad5ea.tar.gz |
Intermediate changes
-rw-r--r-- | contrib/python/beniget/.dist-info/METADATA | 16 | ||||
-rw-r--r-- | contrib/python/beniget/README.rst | 3 | ||||
-rw-r--r-- | contrib/python/beniget/beniget/__init__.py | 1 | ||||
-rw-r--r-- | contrib/python/beniget/beniget/__main__.py | 64 | ||||
-rw-r--r-- | contrib/python/beniget/beniget/beniget.py | 1136 | ||||
-rw-r--r-- | contrib/python/beniget/beniget/ordered_set.py | 86 | ||||
-rw-r--r-- | contrib/python/beniget/beniget/version.py | 1 | ||||
-rw-r--r-- | contrib/python/beniget/ya.make | 5 | ||||
-rw-r--r-- | yt/python/yt/yson/__init__.py | 2 |
9 files changed, 970 insertions, 344 deletions
diff --git a/contrib/python/beniget/.dist-info/METADATA b/contrib/python/beniget/.dist-info/METADATA index eb5452e1f4..522015f48e 100644 --- a/contrib/python/beniget/.dist-info/METADATA +++ b/contrib/python/beniget/.dist-info/METADATA @@ -1,30 +1,24 @@ Metadata-Version: 2.1 Name: beniget -Version: 0.4.1 +Version: 0.4.2.post1 Summary: Extract semantic information about static Python code Home-page: https://github.com/serge-sans-paille/beniget/ Author: serge-sans-paille Author-email: serge.guelton@telecom-bretagne.eu License: BSD 3-Clause -Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* -Requires-Dist: gast (~=0.5.0) +Requires-Python: >=3.6 +License-File: LICENSE +Requires-Dist: gast >=0.5.0 -A static analyzer for Python2 and Python3 code. +A static analyzer for Python code. Beniget provides a static over-approximation of the global and local definitions inside Python Module/Class/Function. It can also compute def-use chains from each definition. - diff --git a/contrib/python/beniget/README.rst b/contrib/python/beniget/README.rst index 19ce6239ac..85746cab88 100644 --- a/contrib/python/beniget/README.rst +++ b/contrib/python/beniget/README.rst @@ -5,8 +5,7 @@ Beniget is a collection of Compile-time analyse on Python Abstract Syntax Tree(A It's a building block to write static analyzer or compiler for Python. Beniget relies on `gast <https://pypi.org/project/gast/>`_ to provide a cross -version abstraction of the AST, effectively working on both Python2 and -Python3. +version abstraction of the AST, effectively working across all Python 3 versions greater than 3.6. API --- diff --git a/contrib/python/beniget/beniget/__init__.py b/contrib/python/beniget/beniget/__init__.py index 73ca4c8a44..8e35334268 100644 --- a/contrib/python/beniget/beniget/__init__.py +++ b/contrib/python/beniget/beniget/__init__.py @@ -1,2 +1,3 @@ from __future__ import absolute_import +from beniget.version import __version__ from beniget.beniget import Ancestors, DefUseChains, UseDefChains diff --git a/contrib/python/beniget/beniget/__main__.py b/contrib/python/beniget/beniget/__main__.py new file mode 100644 index 0000000000..a0d26e3182 --- /dev/null +++ b/contrib/python/beniget/beniget/__main__.py @@ -0,0 +1,64 @@ +import sys + +import gast as ast +from beniget import Ancestors, DefUseChains + +class Beniget(ast.NodeVisitor): + def __init__(self, filename, module): + super(Beniget, self).__init__() + + self.filename = filename or "<stdin>" + + self.ancestors = Ancestors() + self.ancestors.visit(module) + + self.defuses = DefUseChains(self.filename) + self.defuses.visit(module) + + self.visit(module) + + def check_unused(self, node, skipped_types=()): + for local_def in self.defuses.locals[node]: + if not local_def.users(): + if local_def.name() == "_": + continue # typical naming by-pass + if isinstance(local_def.node, skipped_types): + continue + + location = local_def.node + while not hasattr(location, "lineno"): + location = self.ancestors.parent(location) + + if isinstance(location, ast.ImportFrom): + if location.module == "__future__": + continue + + print( + "W: '{}' is defined but not used at {}:{}:{}".format( + local_def.name(), + self.filename, + location.lineno, + location.col_offset, + ) + ) + + def visit_Module(self, node): + self.generic_visit(node) + if self.filename.endswith("__init__.py"): + return + self.check_unused( + node, skipped_types=(ast.FunctionDef, ast.AsyncFunctionDef, + ast.ClassDef, ast.Name) + ) + + def visit_FunctionDef(self, node): + self.generic_visit(node) + self.check_unused(node) + +paths = sys.argv[1:] or (None,) + +for path in paths: + with open(path) if path else sys.stdin as target: + module = ast.parse(target.read()) + Beniget(path, module) + diff --git a/contrib/python/beniget/beniget/beniget.py b/contrib/python/beniget/beniget/beniget.py index 99ad17324b..fb117a2337 100644 --- a/contrib/python/beniget/beniget/beniget.py +++ b/contrib/python/beniget/beniget/beniget.py @@ -1,34 +1,10 @@ -from collections import defaultdict, OrderedDict +from collections import defaultdict from contextlib import contextmanager import sys import gast as ast - -class ordered_set(object): - def __init__(self, elements=None): - self.values = OrderedDict.fromkeys(elements or []) - - def add(self, value): - self.values[value] = None - - def update(self, values): - self.values.update((k, None) for k in values) - - def __iter__(self): - return iter(self.values.keys()) - - def __contains__(self, value): - return value in self.values - - def __add__(self, other): - out = self.values.copy() - out.update(other.values) - return out - - def __len__(self): - return len(self.values) - +from .ordered_set import ordered_set class Ancestors(ast.NodeVisitor): """ @@ -80,17 +56,40 @@ class Ancestors(ast.NodeVisitor): def parentStmt(self, node): return self.parentInstance(node, ast.stmt) +_novalue = object() +@contextmanager +def _rename_attrs(obj, **attrs): + """ + Provide cheap attribute polymorphism. + """ + old_values = {} + for k, v in attrs.items(): + old_values[k] = getattr(obj, k, _novalue) + setattr(obj, k, v) + yield + for k, v in old_values.items(): + if v is _novalue: + delattr(obj, k) + else: + setattr(obj, k, v) class Def(object): """ Model a definition, either named or unnamed, and its users. """ - __slots__ = "node", "_users" + __slots__ = "node", "_users", "islive" def __init__(self, node): self.node = node self._users = ordered_set() + self.islive = True + """ + Whether this definition might reach the final block of it's scope. + Meaning if islive is `False`, the definition will always be overriden + at the time we finished executing the module/class/function body. + So the definition could be ignored in the context of an attribute access for instance. + """ def add_user(self, node): assert isinstance(node, Def) @@ -110,10 +109,18 @@ class Def(object): elif isinstance(self.node, ast.alias): base = self.node.name.split(".", 1)[0] return self.node.asname or base + elif isinstance(self.node, (ast.MatchStar, ast.MatchAs)): + if self.node.name: + return self.node.name + elif isinstance(self.node, ast.MatchMapping): + if self.node.rest: + return self.node.rest + elif isinstance(self.node, ast.Attribute): + return "." + self.node.attr elif isinstance(self.node, tuple): return self.node[1] - else: - return type(self.node).__name__ + + return f'<{type(self.node).__name__}>' def users(self): """ @@ -148,14 +155,8 @@ class Def(object): ) -Builtins = {} - -if sys.version_info.major == 2: - BuiltinsSrc = __builtins__ -else: - import builtins - - BuiltinsSrc = builtins.__dict__ +import builtins +BuiltinsSrc = builtins.__dict__ Builtins = {k: v for k, v in BuiltinsSrc.items()} @@ -163,14 +164,99 @@ Builtins["__file__"] = __file__ DeclarationStep, DefinitionStep = object(), object() +def collect_future_imports(node): + """ + Returns a set of future imports names for the given ast module. + """ + assert isinstance(node, ast.Module) + cf = _CollectFutureImports() + cf.visit(node) + return cf.FutureImports + +class _StopTraversal(Exception): + pass + +class _CollectFutureImports(ast.NodeVisitor): + # A future statement must appear near the top of the module. + # The only lines that can appear before a future statement are: + # - the module docstring (if any), + # - comments, + # - blank lines, and + # - other future statements. + # as soon as we're visiting something else, we can stop the visit. + def __init__(self): + self.FutureImports = set() #type:set[str] + + def visit_Module(self, node): + for child in node.body: + try: + self.visit(child) + except _StopTraversal: + break + + def visit_ImportFrom(self, node): + if node.level or node.module != '__future__': + raise _StopTraversal() + self.FutureImports.update((al.name for al in node.names)) + + def visit_Expr(self, node): + self.visit(node.value) + + def visit_Constant(self, node): + if not isinstance(node.value, str): + raise _StopTraversal() + + def generic_visit(self, node): + raise _StopTraversal() -class CollectGlobals(ast.NodeVisitor): +class CollectLocals(ast.NodeVisitor): def __init__(self): - self.Globals = defaultdict(list) + self.Locals = set() + self.NonLocals = set() - def visit_Global(self, node): - for name in node.names: - self.Globals[name].append((node, name)) + def visit_FunctionDef(self, node): + # no recursion + self.Locals.add(node.name) + + visit_AsyncFunctionDef = visit_FunctionDef + + visit_ClassDef = visit_FunctionDef + + def visit_Nonlocal(self, node): + self.NonLocals.update(name for name in node.names) + + visit_Global = visit_Nonlocal + + def visit_Name(self, node): + if isinstance(node.ctx, ast.Store) and node.id not in self.NonLocals: + self.Locals.add(node.id) + + def skip(self, _): + pass + + visit_SetComp = visit_DictComp = visit_ListComp = skip + visit_GeneratorExp = skip + + visit_Lambda = skip + + def visit_Import(self, node): + for alias in node.names: + base = alias.name.split(".", 1)[0] + self.Locals.add(alias.asname or base) + + def visit_ImportFrom(self, node): + for alias in node.names: + self.Locals.add(alias.asname or alias.name) + +def collect_locals(node): + ''' + Compute the set of identifiers local to a given node. + + This is meant to emulate a call to locals() + ''' + visitor = CollectLocals() + visitor.generic_visit(node) + return visitor.Locals class DefUseChains(ast.NodeVisitor): @@ -190,7 +276,9 @@ class DefUseChains(ast.NodeVisitor): d: 0 >>> alias_def = duc.chains[module.body[0].names[0]] >>> print(alias_def) - c -> (c -> (Call -> ())) + c -> (c -> (<Call> -> ())) + + One instance of DefUseChains is only suitable to analyse one AST Module in it's lifecycle. """ def __init__(self, filename=None): @@ -199,35 +287,66 @@ class DefUseChains(ast.NodeVisitor): """ self.chains = {} self.locals = defaultdict(list) + self.filename = filename # deep copy of builtins, to remain reentrant self._builtins = {k: Def(v) for k, v in Builtins.items()} # function body are not executed when the function definition is met - # this holds a stack of the functions met during body processing + # this holds a list of the functions met during body processing self._defered = [] # stack of mapping between an id and Names self._definitions = [] + # stack of scope depth + self._scope_depths = [] + # stack of variable defined with the global keywords - self._promoted_locals = [] + self._globals = [] + + # stack of local identifiers, used to detect 'read before assign' + self._precomputed_locals = [] # stack of variable that were undefined when we met them, but that may # be defined in another path of the control flow (esp. in loop) self._undefs = [] - # stack of current node holding definitions: class, module, function... - self._currenthead = [] + # stack of nodes starting a scope: class, module, function, generator expression, comprehension... + self._scopes = [] self._breaks = [] self._continues = [] - # dead code levels - self.deadcode = 0 + # stack of list of annotations (annotation, heads, callback), + # only used in the case of from __future__ import annotations feature. + # the annotations are analyzed when the whole module has been processed, + # it should be compatible with PEP 563, and minor changes are required to support PEP 649. + self._defered_annotations = [] + + # dead code levels, it's non null for code that cannot be executed + self._deadcode = 0 - # helpers + # attributes set in visit_Module + self.module = None + self.future_annotations = False + + # + ## helpers + # + def _dump_locals(self, node, only_live=False): + """ + Like `dump_definitions` but returns the result grouped by symbol name and it includes linenos. + + :Returns: List of string formatted like: '{symbol name}:{def lines}' + """ + groupped = defaultdict(list) + for d in self.locals[node]: + if not only_live or d.islive: + groupped[d.name()].append(d) + return ['{}:{}'.format(name, ','.join([str(getattr(d.node, 'lineno', -1)) for d in defs])) \ + for name,defs in groupped.items()] def dump_definitions(self, node, ignore_builtins=True): if isinstance(node, ast.Module) and not ignore_builtins: @@ -243,32 +362,111 @@ class DefUseChains(ast.NodeVisitor): chains.append(str(d)) return chains - def unbound_identifier(self, name, node): + def location(self, node): if hasattr(node, "lineno"): filename = "{}:".format( "<unknown>" if self.filename is None else self.filename ) - location = " at {}{}:{}".format(filename, + return " at {}{}:{}".format(filename, node.lineno, node.col_offset) else: - location = "" - print("W: unbound identifier '{}'{}".format(name, location)) + return "" + + def unbound_identifier(self, name, node): + self.warn("unbound identifier '{}'".format(name), node) + + def warn(self, msg, node): + print("W: {}{}".format(msg, self.location(node))) + + def invalid_name_lookup(self, name, scope, precomputed_locals, local_defs): + # We may hit the situation where we refer to a local variable which is + # not bound yet. This is a runtime error in Python, so we try to detect + # it statically. + + # not a local variable => fine + if name not in precomputed_locals: + return - def lookup_identifier(self, name): - for d in reversed(self._definitions): - if name in d: - return d[name] - return [] + # It's meant to be a local, but can we resolve it by a local lookup? + islocal = any((name in defs or '*' in defs) for defs in local_defs) + + # At class scope, it's ok to refer to a global even if we also have a + # local definition for that variable. Stated other wise + # + # >>> a = 1 + # >>> def foo(): a = a + # >>> foo() # fails, a is a local referenced before being assigned + # >>> class bar: a = a + # >>> bar() # ok, and `bar.a is a` + if isinstance(scope, ast.ClassDef): + top_level_definitions = self._definitions[0:-self._scope_depths[0]] + isglobal = any((name in top_lvl_def or '*' in top_lvl_def) + for top_lvl_def in top_level_definitions) + return not islocal and not isglobal + else: + return not islocal - def defs(self, node): + def compute_annotation_defs(self, node, quiet=False): + name = node.id + # resolving an annotation is a bit different + # form other names. + try: + return lookup_annotation_name_defs(name, self._scopes, self.locals) + except LookupError: + # fallback to regular behaviour on module scope + # to support names from builtins or wildcard imports. + return self.compute_defs(node, quiet=quiet) + + def compute_defs(self, node, quiet=False): + ''' + Performs an actual lookup of node's id in current context, returning + the list of def linked to that use. + ''' name = node.id stars = [] - for d in reversed(self._definitions): - if name in d: - return d[name] if not stars else stars + list(d[name]) - if "*" in d: - stars.extend(d["*"]) + + # If the `global` keyword has been used, honor it + if any(name in _globals for _globals in self._globals): + looked_up_definitions = self._definitions[0:-self._scope_depths[0]] + else: + # List of definitions to check. This includes all non-class + # definitions *and* the last definition. Class definitions are not + # included because they require fully qualified access. + looked_up_definitions = [] + + scopes_iter = iter(reversed(self._scopes)) + depths_iter = iter(reversed(self._scope_depths)) + precomputed_locals_iter = iter(reversed(self._precomputed_locals)) + + # Keep the last scope because we could be in class scope, in which + # case we don't need fully qualified access. + lvl = depth = next(depths_iter) + precomputed_locals = next(precomputed_locals_iter) + base_scope = next(scopes_iter) + defs = self._definitions[depth:] + if not self.invalid_name_lookup(name, base_scope, precomputed_locals, defs): + looked_up_definitions.extend(reversed(defs)) + + # Iterate over scopes, filtering out class scopes. + for scope, depth, precomputed_locals in zip(scopes_iter, + depths_iter, + precomputed_locals_iter): + if not isinstance(scope, ast.ClassDef): + defs = self._definitions[lvl + depth: lvl] + if self.invalid_name_lookup(name, base_scope, precomputed_locals, defs): + looked_up_definitions.append(StopIteration) + break + looked_up_definitions.extend(reversed(defs)) + lvl += depth + + for defs in looked_up_definitions: + if defs is StopIteration: + break + elif name in defs: + return defs[name] if not stars else stars + list(defs[name]) + elif "*" in defs: + stars.extend(defs["*"]) d = self.chains.setdefault(node, Def(node)) @@ -278,20 +476,22 @@ class DefUseChains(ast.NodeVisitor): if stars: return stars + [d] else: - if not self._undefs: + if not self._undefs and not quiet: self.unbound_identifier(name, node) return [d] + defs = compute_defs + def process_body(self, stmts): deadcode = False for stmt in stmts: + self.visit(stmt) if isinstance(stmt, (ast.Break, ast.Continue, ast.Raise)): if not deadcode: deadcode = True - self.deadcode += 1 - self.visit(stmt) + self._deadcode += 1 if deadcode: - self.deadcode -= 1 + self._deadcode -= 1 def process_undefs(self): for undef_name, _undefs in self._undefs[-1].items(): @@ -307,57 +507,85 @@ class DefUseChains(ast.NodeVisitor): self._undefs.pop() @contextmanager - def DefinitionContext(self, node): - self._currenthead.append(node) + def ScopeContext(self, node): + self._scopes.append(node) + self._scope_depths.append(-1) self._definitions.append(defaultdict(ordered_set)) - self._promoted_locals.append(set()) + self._globals.append(set()) + self._precomputed_locals.append(collect_locals(node)) yield - self._promoted_locals.pop() + self._precomputed_locals.pop() + self._globals.pop() + self._definitions.pop() + self._scope_depths.pop() + self._scopes.pop() + + CompScopeContext = ScopeContext + + @contextmanager + def DefinitionContext(self, definitions): + self._definitions.append(definitions) + self._scope_depths[-1] -= 1 + yield self._definitions[-1] + self._scope_depths[-1] += 1 self._definitions.pop() - self._currenthead.pop() + @contextmanager - def CompDefinitionContext(self, node): - if sys.version_info.major >= 3: - self._currenthead.append(node) - self._definitions.append(defaultdict(ordered_set)) - self._promoted_locals.append(set()) + def SwitchScopeContext(self, defs, scopes, scope_depths, precomputed_locals): + scope_depths, self._scope_depths = self._scope_depths, scope_depths + scopes, self._scopes = self._scopes, scopes + defs, self._definitions = self._definitions, defs + precomputed_locals, self._precomputed_locals = self._precomputed_locals, precomputed_locals yield - if sys.version_info.major >= 3: - self._promoted_locals.pop() - self._definitions.pop() - self._currenthead.pop() + self._definitions = defs + self._scopes = scopes + self._scope_depths = scope_depths + self._precomputed_locals = precomputed_locals + + def process_functions_bodies(self): + for fnode, defs, scopes, scope_depths, precomputed_locals in self._defered: + visitor = getattr(self, + "visit_{}".format(type(fnode).__name__)) + with self.SwitchScopeContext(defs, scopes, scope_depths, precomputed_locals): + visitor(fnode, step=DefinitionStep) + + def process_annotations(self): + compute_defs, self.defs = self.defs, self.compute_annotation_defs + for annnode, heads, cb in self._defered_annotations[-1]: + visitor = getattr(self, + "visit_{}".format(type(annnode).__name__)) + currenthead, self._scopes = self._scopes, heads + cb(visitor(annnode)) if cb else visitor(annnode) + self._scopes = currenthead + self.defs = compute_defs # stmt def visit_Module(self, node): self.module = node - with self.DefinitionContext(node): + + futures = collect_future_imports(node) + # determine whether the PEP563 is enabled + # allow manual enabling of DefUseChains.future_annotations + self.future_annotations |= 'annotations' in futures + + + with self.ScopeContext(node): + self._definitions[-1].update( {k: ordered_set((v,)) for k, v in self._builtins.items()} ) - self._defered.append([]) + self._defered_annotations.append([]) self.process_body(node.body) - # handle `global' keyword specifically - cg = CollectGlobals() - cg.visit(node) - for nodes in cg.Globals.values(): - for n, name in nodes: - if name not in self._definitions[-1]: - dnode = Def((n, name)) - self.set_definition(name, dnode) - self.locals[node].append(dnode) - # handle function bodies - for fnode, ctx in self._defered[-1]: - visitor = getattr(self, - "visit_{}".format(type(fnode).__name__)) - defs, self._definitions = self._definitions, ctx - visitor(fnode, step=DefinitionStep) - self._definitions = defs - self._defered.pop() + self.process_functions_bodies() + + # handle defered annotations as in from __future__ import annotations + self.process_annotations() + self._defered_annotations.pop() # various sanity checks if __debug__: @@ -375,15 +603,36 @@ class DefUseChains(ast.NodeVisitor): assert nb_defs == nb_heads + nb_bltns - nb_overloaded_bltns assert not self._definitions - assert not self._defered + assert not self._defered_annotations + assert not self._scopes + assert not self._scope_depths + assert not self._precomputed_locals - def set_definition(self, name, dnode_or_dnodes): - if self.deadcode: + def set_definition(self, name, dnode_or_dnodes, index=-1): + if self._deadcode: return + if isinstance(dnode_or_dnodes, Def): - self._definitions[-1][name] = ordered_set((dnode_or_dnodes,)) + dnodes = ordered_set((dnode_or_dnodes,)) else: - self._definitions[-1][name] = ordered_set(dnode_or_dnodes) + dnodes = ordered_set(dnode_or_dnodes) + + # set the islive flag to False on killed Defs + for d in self._definitions[index].get(name, ()): + if not isinstance(d.node, ast.AST): + # A builtin: we never explicitely mark the builtins as killed, since + # it can be easily deducted. + continue + if d in dnodes or any(d in definitions.get(name, ()) for + definitions in self._definitions[:index]): + # The definition exists in another definition context, so we can't + # be sure wether it's killed or not, this happens when: + # - a variable is conditionnaly declared (d in dnodes) + # - a variable is conditionnaly killed (any(...)) + continue + d.islive = False + + self._definitions[index][name] = dnodes @staticmethod def add_to_definition(definition, name, dnode_or_dnodes): @@ -393,16 +642,62 @@ class DefUseChains(ast.NodeVisitor): definition[name].update(dnode_or_dnodes) def extend_definition(self, name, dnode_or_dnodes): - if self.deadcode: + if self._deadcode: return DefUseChains.add_to_definition(self._definitions[-1], name, dnode_or_dnodes) + def extend_global(self, name, dnode_or_dnodes): + if self._deadcode: + return + # `name` *should* be in self._definitions[0] because we extend the + # globals. Yet the original code maybe faulty and we need to cope with + # it. + if name not in self._definitions[0]: + if isinstance(dnode_or_dnodes, Def): + self.locals[self.module].append(dnode_or_dnodes) + else: + self.locals[self.module].extend(dnode_or_dnodes) + DefUseChains.add_to_definition(self._definitions[0], name, + dnode_or_dnodes) + + def set_or_extend_global(self, name, dnode): + if self._deadcode: + return + if name not in self._definitions[0]: + self.locals[self.module].append(dnode) + DefUseChains.add_to_definition(self._definitions[0], name, dnode) + + def visit_annotation(self, node): + annotation = getattr(node, 'annotation', None) + if annotation: + self.visit(annotation) + + def visit_skip_annotation(self, node): + if isinstance(node, ast.Name): + self.visit_Name(node, skip_annotation=True) + else: + self.visit(node) + def visit_FunctionDef(self, node, step=DeclarationStep): if step is DeclarationStep: dnode = self.chains.setdefault(node, Def(node)) - self.set_definition(node.name, dnode) - self.locals[self._currenthead[-1]].append(dnode) + self.add_to_locals(node.name, dnode) + + if not self.future_annotations: + for arg in _iter_arguments(node.args): + self.visit_annotation(arg) + + else: + # annotations are to be analyzed later as well + currentscopes = list(self._scopes) + if node.returns: + self._defered_annotations[-1].append( + (node.returns, currentscopes, None)) + for arg in _iter_arguments(node.args): + if arg.annotation: + self._defered_annotations[-1].append( + (arg.annotation, currentscopes, None)) for kw_default in filter(None, node.args.kw_defaults): self.visit(kw_default).add_user(dnode) @@ -411,16 +706,20 @@ class DefUseChains(ast.NodeVisitor): for decorator in node.decorator_list: self.visit(decorator) - definitions = list(self._definitions) - if isinstance(self._currenthead[-1], ast.ClassDef): - definitions.pop() - self._defered[-1].append((node, definitions)) - elif step is DefinitionStep: - # function is not considered as defined when evaluating returns - if node.returns: + if not self.future_annotations and node.returns: self.visit(node.returns) - with self.DefinitionContext(node): - self.visit(node.args) + + self.set_definition(node.name, dnode) + + self._defered.append((node, + list(self._definitions), + list(self._scopes), + list(self._scope_depths), + list(self._precomputed_locals))) + elif step is DefinitionStep: + with self.ScopeContext(node): + for arg in _iter_arguments(node.args): + self.visit_skip_annotation(arg) self.process_body(node.body) else: raise NotImplementedError() @@ -429,8 +728,8 @@ class DefUseChains(ast.NodeVisitor): def visit_ClassDef(self, node): dnode = self.chains.setdefault(node, Def(node)) - self.locals[self._currenthead[-1]].append(dnode) - self.set_definition(node.name, dnode) + self.add_to_locals(node.name, dnode) + for base in node.bases: self.visit(base).add_user(dnode) for keyword in node.keywords: @@ -438,10 +737,13 @@ class DefUseChains(ast.NodeVisitor): for decorator in node.decorator_list: self.visit(decorator).add_user(dnode) - with self.DefinitionContext(node): + with self.ScopeContext(node): self.set_definition("__class__", Def("__class__")) self.process_body(node.body) + self.set_definition(node.name, dnode) + + def visit_Return(self, node): if node.value: self.visit(node.value) @@ -468,12 +770,13 @@ class DefUseChains(ast.NodeVisitor): def visit_AnnAssign(self, node): if node.value: - dvalue = self.visit(node.value) - dannotation = self.visit(node.annotation) - dtarget = self.visit(node.target) - dtarget.add_user(dannotation) - if node.value: - dvalue.add_user(dtarget) + self.visit(node.value) + if not self.future_annotations: + self.visit(node.annotation) + else: + self._defered_annotations[-1].append( + (node.annotation, list(self._scopes), None)) + self.visit(node.target) def visit_AugAssign(self, node): dvalue = self.visit(node.value) @@ -482,24 +785,19 @@ class DefUseChains(ast.NodeVisitor): dtarget = self.visit(node.target) dvalue.add_user(dtarget) node.target.ctx = ctx - if node.target.id in self._promoted_locals[-1]: - self.extend_definition(node.target.id, dtarget) + if any(node.target.id in _globals for _globals in self._globals): + self.extend_global(node.target.id, dtarget) else: - loaded_from = [d.name() for d in self.defs(node.target)] + loaded_from = [d.name() for d in self.defs(node.target, + quiet=True)] self.set_definition(node.target.id, dtarget) # If we augassign from a value that comes from '*', let's use # this node as the definition point. if '*' in loaded_from: - self.locals[self._currenthead[-1]].append(dtarget) + self.locals[self._scopes[-1]].append(dtarget) else: self.visit(node.target).add_user(dvalue) - def visit_Print(self, node): - if node.dest: - self.visit(node.dest) - for value in node.values: - self.visit(value) - def visit_For(self, node): self.visit(node.iter) @@ -507,29 +805,27 @@ class DefUseChains(ast.NodeVisitor): self._continues.append(defaultdict(ordered_set)) self._undefs.append(defaultdict(list)) - self._definitions.append(self._definitions[-1].copy()) - self.visit(node.target) - self.process_body(node.body) - self.process_undefs() + with self.DefinitionContext(self._definitions[-1].copy()) as body_defs: + self.visit(node.target) + self.process_body(node.body) + self.process_undefs() - continue_defs = self._continues.pop() - for d, u in continue_defs.items(): - self.extend_definition(d, u) - self._continues.append(defaultdict(ordered_set)) + continue_defs = self._continues.pop() + for d, u in continue_defs.items(): + self.extend_definition(d, u) + self._continues.append(defaultdict(ordered_set)) - # extra round to ``emulate'' looping - self.visit(node.target) - self.process_body(node.body) + # extra round to ``emulate'' looping + self.visit(node.target) + self.process_body(node.body) - # process else clause in case of late break - self._definitions.append(defaultdict(ordered_set)) - self.process_body(node.orelse) - orelse_defs = self._definitions.pop() + # process else clause in case of late break + with self.DefinitionContext(defaultdict(ordered_set)) as orelse_defs: + self.process_body(node.orelse) - break_defs = self._breaks.pop() - continue_defs = self._continues.pop() + break_defs = self._breaks.pop() + continue_defs = self._continues.pop() - body_defs = self._definitions.pop() for d, u in orelse_defs.items(): self.extend_definition(d, u) @@ -547,39 +843,35 @@ class DefUseChains(ast.NodeVisitor): def visit_While(self, node): - self._definitions.append(self._definitions[-1].copy()) - self._undefs.append(defaultdict(list)) - self._breaks.append(defaultdict(ordered_set)) - self._continues.append(defaultdict(ordered_set)) - - self.process_body(node.orelse) + with self.DefinitionContext(self._definitions[-1].copy()): + self._undefs.append(defaultdict(list)) + self._breaks.append(defaultdict(ordered_set)) + self._continues.append(defaultdict(ordered_set)) - self._definitions.pop() + self.process_body(node.orelse) - self._definitions.append(self._definitions[-1].copy()) + with self.DefinitionContext(self._definitions[-1].copy()) as body_defs: - self.visit(node.test) - self.process_body(node.body) + self.visit(node.test) + self.process_body(node.body) - self.process_undefs() + self.process_undefs() - continue_defs = self._continues.pop() - for d, u in continue_defs.items(): - self.extend_definition(d, u) - self._continues.append(defaultdict(ordered_set)) + continue_defs = self._continues.pop() + for d, u in continue_defs.items(): + self.extend_definition(d, u) + self._continues.append(defaultdict(ordered_set)) - # extra round to simulate loop - self.visit(node.test) - self.process_body(node.body) + # extra round to simulate loop + self.visit(node.test) + self.process_body(node.body) - # the false branch of the eval - self.visit(node.test) + # the false branch of the eval + self.visit(node.test) - self._definitions.append(self._definitions[-1].copy()) - self.process_body(node.orelse) + with self.DefinitionContext(self._definitions[-1].copy()) as orelse_defs: + self.process_body(node.orelse) - orelse_defs = self._definitions.pop() - body_defs = self._definitions.pop() break_defs = self._breaks.pop() continue_defs = self._continues.pop() @@ -599,13 +891,12 @@ class DefUseChains(ast.NodeVisitor): self.visit(node.test) # putting a copy of current level to handle nested conditions - self._definitions.append(self._definitions[-1].copy()) - self.process_body(node.body) - body_defs = self._definitions.pop() + with self.DefinitionContext(self._definitions[-1].copy()) as body_defs: + self.process_body(node.body) + + with self.DefinitionContext(self._definitions[-1].copy()) as orelse_defs: + self.process_body(node.orelse) - self._definitions.append(self._definitions[-1].copy()) - self.process_body(node.orelse) - orelse_defs = self._definitions.pop() for d in body_defs: if d in orelse_defs: self.set_definition(d, body_defs[d] + orelse_defs[d]) @@ -626,81 +917,64 @@ class DefUseChains(ast.NodeVisitor): visit_AsyncWith = visit_With def visit_Raise(self, node): - if node.exc: - self.visit(node.exc) - if node.cause: - self.visit(node.cause) + self.generic_visit(node) def visit_Try(self, node): - self._definitions.append(self._definitions[-1].copy()) - self.process_body(node.body) - self.process_body(node.orelse) - failsafe_defs = self._definitions.pop() + with self.DefinitionContext(self._definitions[-1].copy()) as failsafe_defs: + self.process_body(node.body) + self.process_body(node.orelse) # handle the fact that definitions may have fail for d in failsafe_defs: self.extend_definition(d, failsafe_defs[d]) for excepthandler in node.handlers: - self._definitions.append(defaultdict(ordered_set)) - self.visit(excepthandler) - handler_def = self._definitions.pop() + with self.DefinitionContext(defaultdict(ordered_set)) as handler_def: + self.visit(excepthandler) + for hd in handler_def: self.extend_definition(hd, handler_def[hd]) self.process_body(node.finalbody) + visit_TryStar = visit_Try + def visit_Assert(self, node): self.visit(node.test) if node.msg: self.visit(node.msg) + def add_to_locals(self, name, dnode): + if any(name in _globals for _globals in self._globals): + self.set_or_extend_global(name, dnode) + else: + self.locals[self._scopes[-1]].append(dnode) + def visit_Import(self, node): for alias in node.names: dalias = self.chains.setdefault(alias, Def(alias)) base = alias.name.split(".", 1)[0] self.set_definition(alias.asname or base, dalias) - self.locals[self._currenthead[-1]].append(dalias) + self.add_to_locals(alias.asname or base, dalias) def visit_ImportFrom(self, node): for alias in node.names: dalias = self.chains.setdefault(alias, Def(alias)) - self.set_definition(alias.asname or alias.name, dalias) - self.locals[self._currenthead[-1]].append(dalias) - - def visit_Exec(self, node): - dnode = self.chains.setdefault(node, Def(node)) - self.visit(node.body) - - if node.globals: - self.visit(node.globals) - else: - # any global may be used by this exec! - for defs in self._definitions[0].values(): - for d in defs: - d.add_user(dnode) - - if node.locals: - self.visit(node.locals) - else: - # any local may be used by this exec! - visible_locals = set() - for _definitions in reversed(self._definitions[1:]): - for dname, defs in _definitions.items(): - if dname not in visible_locals: - visible_locals.add(dname) - for d in defs: - d.add_user(dnode) - - self.extend_definition("*", dnode) + if alias.name == '*': + self.extend_definition('*', dalias) + else: + self.set_definition(alias.asname or alias.name, dalias) + self.add_to_locals(alias.asname or alias.name, dalias) def visit_Global(self, node): for name in node.names: - self._promoted_locals[-1].add(name) + self._globals[-1].add(name) def visit_Nonlocal(self, node): for name in node.names: - for d in reversed(self._definitions[:-1]): + # Exclude global scope + global_scope_depth = -self._scope_depths[0] + for d in reversed(self._definitions[global_scope_depth: -1]): if name not in d: continue else: @@ -734,12 +1008,20 @@ class DefUseChains(ast.NodeVisitor): def visit_Lambda(self, node, step=DeclarationStep): if step is DeclarationStep: dnode = self.chains.setdefault(node, Def(node)) - self._defered[-1].append((node, list(self._definitions))) + for default in node.args.defaults: + self.visit(default).add_user(dnode) + # a lambda never has kw_defaults + self._defered.append((node, + list(self._definitions), + list(self._scopes), + list(self._scope_depths), + list(self._precomputed_locals))) return dnode elif step is DefinitionStep: dnode = self.chains[node] - with self.DefinitionContext(node): - self.visit(node.args) + with self.ScopeContext(node): + for a in _iter_arguments(node.args): + self.visit(a) self.visit(node.body).add_user(dnode) return dnode else: @@ -768,10 +1050,15 @@ class DefUseChains(ast.NodeVisitor): def visit_ListComp(self, node): dnode = self.chains.setdefault(node, Def(node)) - - with self.CompDefinitionContext(node): - for comprehension in node.generators: - self.visit(comprehension).add_user(dnode) + try: + _validate_comprehension(node) + except SyntaxError as e: + self.warn(str(e), node) + return dnode + with self.CompScopeContext(node): + for i, comprehension in enumerate(node.generators): + self.visit_comprehension(comprehension, + is_nested=i!=0).add_user(dnode) self.visit(node.elt).add_user(dnode) return dnode @@ -780,10 +1067,15 @@ class DefUseChains(ast.NodeVisitor): def visit_DictComp(self, node): dnode = self.chains.setdefault(node, Def(node)) - - with self.CompDefinitionContext(node): - for comprehension in node.generators: - self.visit(comprehension).add_user(dnode) + try: + _validate_comprehension(node) + except SyntaxError as e: + self.warn(str(e), node) + return dnode + with self.CompScopeContext(node): + for i, comprehension in enumerate(node.generators): + self.visit_comprehension(comprehension, + is_nested=i!=0).add_user(dnode) self.visit(node.key).add_user(dnode) self.visit(node.value).add_user(dnode) @@ -847,30 +1139,61 @@ class DefUseChains(ast.NodeVisitor): self.visit(node.slice).add_user(dnode) return dnode - visit_Starred = visit_Await + def visit_Starred(self, node): + if isinstance(node.ctx, ast.Store): + return self.visit(node.value) + else: + dnode = self.chains.setdefault(node, Def(node)) + self.visit(node.value).add_user(dnode) + return dnode def visit_NamedExpr(self, node): dnode = self.chains.setdefault(node, Def(node)) self.visit(node.value).add_user(dnode) - self.visit(node.target) + if isinstance(node.target, ast.Name): + self.visit_Name(node.target, named_expr=True) return dnode - def visit_Name(self, node): + def is_in_current_scope(self, name): + return any(name in defs + for defs in self._definitions[self._scope_depths[-1]:]) + def _first_non_comprehension_scope(self): + index = -1 + enclosing_scope = self._scopes[index] + while isinstance(enclosing_scope, (ast.DictComp, ast.ListComp, + ast.SetComp, ast.GeneratorExp)): + index -= 1 + enclosing_scope = self._scopes[index] + return index, enclosing_scope + + def visit_Name(self, node, skip_annotation=False, named_expr=False): if isinstance(node.ctx, (ast.Param, ast.Store)): dnode = self.chains.setdefault(node, Def(node)) - if node.id in self._promoted_locals[-1]: - self.extend_definition(node.id, dnode) - if dnode not in self.locals[self.module]: - self.locals[self.module].append(dnode) + # FIXME: find a smart way to merge the code below with add_to_locals + if any(node.id in _globals for _globals in self._globals): + self.set_or_extend_global(node.id, dnode) else: - self.set_definition(node.id, dnode) - if dnode not in self.locals[self._currenthead[-1]]: - self.locals[self._currenthead[-1]].append(dnode) - - if node.annotation is not None: + # special code for warlus target: should be + # stored in first non comprehension scope + index, enclosing_scope = (self._first_non_comprehension_scope() + if named_expr else (-1, self._scopes[-1])) + + if index < -1 and isinstance(enclosing_scope, ast.ClassDef): + # invalid named expression, not calling set_definition. + self.warn('assignment expression within a comprehension ' + 'cannot be used in a class body', node) + return dnode + + self.set_definition(node.id, dnode, index) + if dnode not in self.locals[self._scopes[index]]: + self.locals[self._scopes[index]].append(dnode) + + # Name.annotation is a special case because of gast + if node.annotation is not None and not skip_annotation and not self.future_annotations: self.visit(node.annotation) + elif isinstance(node.ctx, (ast.Load, ast.Del)): node_in_chains = node in self.chains if node_in_chains: @@ -894,7 +1217,7 @@ class DefUseChains(ast.NodeVisitor): tmp_store, elt.ctx = elt.ctx, tmp_store self.visit(elt) tmp_store, elt.ctx = elt.ctx, tmp_store - elif isinstance(elt, ast.Subscript): + elif isinstance(elt, (ast.Subscript, ast.Starred, ast.Attribute)): self.visit(elt) elif isinstance(elt, (ast.List, ast.Tuple)): self.visit_Destructured(elt) @@ -927,9 +1250,18 @@ class DefUseChains(ast.NodeVisitor): # misc - def visit_comprehension(self, node): + def visit_comprehension(self, node, is_nested): dnode = self.chains.setdefault(node, Def(node)) - self.visit(node.iter).add_user(dnode) + if not is_nested: + # There's one part of a comprehension or generator expression that executes in the surrounding scope, + # it's the expression for the outermost iterable. + with self.SwitchScopeContext(self._definitions[:-1], self._scopes[:-1], + self._scope_depths[:-1], self._precomputed_locals[:-1]): + self.visit(node.iter).add_user(dnode) + else: + # If a comprehension has multiple for clauses, + # the iterables of the inner for clauses are evaluated in the comprehension's scope: + self.visit(node.iter).add_user(dnode) self.visit(node.target) for if_ in node.ifs: self.visit(if_).add_user(dnode) @@ -945,20 +1277,9 @@ class DefUseChains(ast.NodeVisitor): return dnode def visit_arguments(self, node): - for arg in node.args: + for arg in _iter_arguments(node): self.visit(arg) - for arg in node.posonlyargs: - self.visit(arg) - - if node.vararg: - self.visit(node.vararg) - - for arg in node.kwonlyargs: - self.visit(arg) - if node.kwarg: - self.visit(node.kwarg) - def visit_withitem(self, node): dnode = self.chains.setdefault(node, Def(node)) self.visit(node.context_expr).add_user(dnode) @@ -966,6 +1287,226 @@ class DefUseChains(ast.NodeVisitor): self.visit(node.optional_vars) return dnode + # pattern + + def visit_Match(self, node): + + self.visit(node.subject) + + defs = [] + for kase in node.cases: + if kase.guard: + self.visit(kase.guard) + self.visit(kase.pattern) + + with self.DefinitionContext(self._definitions[-1].copy()) as case_defs: + self.process_body(kase.body) + defs.append(case_defs) + + if len(defs) < 2: + defs += [[]] * (2 - len(defs)) + + body_defs, orelse_defs, *rest = defs + + while True: + # merge defs, like in if-else but repeat the process for x branches + for d in body_defs: + if d in orelse_defs: + self.set_definition(d, body_defs[d] + orelse_defs[d]) + else: + self.extend_definition(d, body_defs[d]) + + for d in orelse_defs: + if d in body_defs: + pass # already done in the previous loop + else: + self.extend_definition(d, orelse_defs[d]) + + if not rest: + break + + body_defs = self._definitions[-1] + orelse_defs, *rest = rest + + def visit_MatchValue(self, node): + dnode = self.chains.setdefault(node, Def(node)) + self.visit(node.value) + return dnode + + visit_MatchSingleton = visit_MatchValue + + def visit_MatchSequence(self, node): + # mimics a list + with _rename_attrs(node, ctx=ast.Load(), elts=node.patterns): + return self.visit_List(node) + + def visit_MatchMapping(self, node): + dnode = self.chains.setdefault(node, Def(node)) + with _rename_attrs(node, values=node.patterns): + # mimics a dict + self.visit_Dict(node) + if node.rest: + with _rename_attrs(node, id=node.rest, ctx=ast.Store(), annotation=None): + self.visit_Name(node) + return dnode + + def visit_MatchClass(self, node): + # mimics a call + dnode = self.chains.setdefault(node, Def(node)) + self.visit(node.cls).add_user(dnode) + for arg in node.patterns: + self.visit(arg).add_user(dnode) + for kw in node.kwd_patterns: + self.visit(kw).add_user(dnode) + return dnode + + def visit_MatchStar(self, node): + dnode = self.chains.setdefault(node, Def(node)) + if node.name: + # mimics store name + with _rename_attrs(node, id=node.name, ctx=ast.Store(), annotation=None): + self.visit_Name(node) + return dnode + + def visit_MatchAs(self, node): + dnode = self.chains.setdefault(node, Def(node)) + if node.pattern: + self.visit(node.pattern) + if node.name: + with _rename_attrs(node, id=node.name, ctx=ast.Store(), annotation=None): + self.visit_Name(node) + return dnode + + def visit_MatchOr(self, node): + dnode = self.chains.setdefault(node, Def(node)) + for pat in node.patterns: + self.visit(pat).add_user(dnode) + return dnode + + +def _validate_comprehension(node): + """ + Raises SyntaxError if: + - a named expression is used in a comprehension iterable expression + - a named expression rebinds a comprehension iteration variable + """ + iter_names = set() # comprehension iteration variables + for gen in node.generators: + for namedexpr in (n for n in ast.walk(gen.iter) if isinstance(n, ast.NamedExpr)): + raise SyntaxError('assignment expression cannot be used ' + 'in a comprehension iterable expression') + iter_names.update(n.id for n in ast.walk(gen.target) + if isinstance(n, ast.Name) and isinstance(n.ctx, ast.Store)) + for namedexpr in (n for n in ast.walk(node) if isinstance(n, ast.NamedExpr)): + bound = getattr(namedexpr.target, 'id', None) + if bound in iter_names: + raise SyntaxError('assignment expression cannot rebind ' + "comprehension iteration variable '{}'".format(bound)) + +def _iter_arguments(args): + """ + Yields all arguments of the given ast.arguments instance. + """ + for arg in args.args: + yield arg + for arg in args.posonlyargs: + yield arg + if args.vararg: + yield args.vararg + for arg in args.kwonlyargs: + yield arg + if args.kwarg: + yield args.kwarg + +def lookup_annotation_name_defs(name, heads, locals_map): + r""" + Simple identifier -> defs resolving. + + Lookup a name with the provided head nodes using the locals_map. + Note that nonlocal and global keywords are ignored by this function. + Only used to resolve annotations when PEP 563 is enabled. + + :param name: The identifier we're looking up. + :param heads: List of ast scope statement that describe + the path to the name context. i.e ``[<Module>, <ClassDef>, <FunctionDef>]``. + The lookup will happend in the context of the body of tail of ``heads`` + Can be gathered with `Ancestors.parents`. + :param locals_map: `DefUseChains.locals`. + + :raise LookupError: For + - builtin names + - wildcard imported names + - unbound names + + :raise ValueError: When the heads is empty. + + This function can be used by client code like this: + + >>> import gast as ast + >>> module = ast.parse("from b import c;import typing as t\nclass C:\n def f(self):self.var = c.Thing()") + >>> duc = DefUseChains() + >>> duc.visit(module) + >>> ancestors = Ancestors() + >>> ancestors.visit(module) + ... # we're placing ourselves in the context of the function body + >>> fn_scope = module.body[-1].body[-1] + >>> assert isinstance(fn_scope, ast.FunctionDef) + >>> heads = ancestors.parents(fn_scope) + [fn_scope] + >>> print(lookup_annotation_name_defs('t', heads, duc.locals)[0]) + t -> () + >>> print(lookup_annotation_name_defs('c', heads, duc.locals)[0]) + c -> (c -> (.Thing -> (<Call> -> ()))) + >>> print(lookup_annotation_name_defs('C', heads, duc.locals)[0]) + C -> () + """ + scopes = _get_lookup_scopes(heads) + scopes_len = len(scopes) + if scopes_len>1: + # start by looking at module scope first, + # then try the theoretical runtime scopes. + # putting the global scope last in the list so annotation are + # resolve using he global namespace first. this is the way pyright does. + scopes.append(scopes.pop(0)) + try: + return _lookup(name, scopes, locals_map) + except LookupError: + raise LookupError("'{}' not found in {}, might be a builtin".format(name, heads[-1])) + +def _get_lookup_scopes(heads): + # heads[-1] is the direct enclosing scope and heads[0] is the module. + # returns a list based on the elements of heads, but with + # the ignorable scopes removed. Ignorable in the sens that the lookup + # will never happend in this scope for the given context. + + heads = list(heads) # avoid modifying the list (important) + try: + direct_scope = heads.pop(-1) # this scope is the only one that can be a class + except IndexError: + raise ValueError('invalid heads: must include at least one element') + try: + global_scope = heads.pop(0) + except IndexError: + # we got only a global scope + return [direct_scope] + # more of less modeling what's described here. + # https://github.com/gvanrossum/gvanrossum.github.io/blob/main/formal/scopesblog.md + other_scopes = [s for s in heads if isinstance(s, ( + ast.FunctionDef, ast.AsyncFunctionDef, + ast.Lambda, ast.DictComp, ast.ListComp, + ast.SetComp, ast.GeneratorExp))] + return [global_scope] + other_scopes + [direct_scope] + +def _lookup(name, scopes, locals_map): + context = scopes.pop() + defs = [] + for loc in locals_map.get(context, ()): + if loc.name() == name and loc.islive: + defs.append(loc) + if defs: + return defs + elif len(scopes)==0: + raise LookupError() + return _lookup(name, scopes, locals_map) class UseDefChains(object): """ @@ -997,66 +1538,3 @@ class UseDefChains(object): out.append((kname, kstr)) out.sort() return ", ".join(s for k, s in out) - - -if __name__ == "__main__": - import sys - - class Beniget(ast.NodeVisitor): - def __init__(self, filename, module): - super(Beniget, self).__init__() - - self.filename = filename or "<stdin>" - - self.ancestors = Ancestors() - self.ancestors.visit(module) - - self.defuses = DefUseChains(self.filename) - self.defuses.visit(module) - - self.visit(module) - - def check_unused(self, node, skipped_types=()): - for local_def in self.defuses.locals[node]: - if not local_def.users(): - if local_def.name() == "_": - continue # typical naming by-pass - if isinstance(local_def.node, skipped_types): - continue - - location = local_def.node - while not hasattr(location, "lineno"): - location = self.ancestors.parent(location) - - if isinstance(location, ast.ImportFrom): - if location.module == "__future__": - continue - - print( - "W: '{}' is defined but not used at {}:{}:{}".format( - local_def.name(), - self.filename, - location.lineno, - location.col_offset, - ) - ) - - def visit_Module(self, node): - self.generic_visit(node) - if self.filename.endswith("__init__.py"): - return - self.check_unused( - node, skipped_types=(ast.FunctionDef, ast.AsyncFunctionDef, - ast.ClassDef, ast.Name) - ) - - def visit_FunctionDef(self, node): - self.generic_visit(node) - self.check_unused(node) - - paths = sys.argv[1:] or (None,) - - for path in paths: - with open(path) if path else sys.stdin as target: - module = ast.parse(target.read()) - Beniget(path, module) diff --git a/contrib/python/beniget/beniget/ordered_set.py b/contrib/python/beniget/beniget/ordered_set.py new file mode 100644 index 0000000000..4a2132c416 --- /dev/null +++ b/contrib/python/beniget/beniget/ordered_set.py @@ -0,0 +1,86 @@ +""" +Copied from https://github.com/bustawin/ordered-set-37 +""" +# Unlicense +# This is free and unencumbered software released into the public domain. + +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. + +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +# For more information, please refer to <http://unlicense.org/> + + +import sys + +from collections import OrderedDict +import itertools +from typing import TYPE_CHECKING, MutableSet + +if TYPE_CHECKING: + # trying to avoid polluting the global namespace with typing names. + from typing import TypeVar, Iterator, Iterable, Optional + T = TypeVar("T") + +class ordered_set(MutableSet['T']): + """ + A set that preserves insertion order by internally using a dict. + """ + + __slots__ = ('values',) + + def __init__(self, elements: 'Optional[Iterable[T]]' = None): + self.values = OrderedDict.fromkeys(elements or []) + + def add(self, x: 'T') -> None: + self.values[x] = None + + def update(self, values:'Iterable[T]') -> None: + self.values.update((k, None) for k in values) + + def clear(self) -> None: + self.values.clear() + + def discard(self, x: 'T') -> None: + self.values.pop(x, None) + + def __getitem__(self, index:int) -> 'T': + try: + return next(itertools.islice(self.values, index, index + 1)) + except StopIteration: + raise IndexError(f"index {index} out of range") + + def __contains__(self, x: object) -> bool: + return self.values.__contains__(x) + + def __add__(self, other:'ordered_set[T]') -> 'ordered_set[T]': + return ordered_set(itertools.chain(self, other)) + + def __len__(self) -> int: + return self.values.__len__() + + def __iter__(self) -> 'Iterator[T]': + return self.values.__iter__() + + def __str__(self) -> str: + return f"{{{', '.join(str(i) for i in self)}}}" + + def __repr__(self) -> str: + return f"<ordered_set {self}>"
\ No newline at end of file diff --git a/contrib/python/beniget/beniget/version.py b/contrib/python/beniget/beniget/version.py new file mode 100644 index 0000000000..aaf01253d2 --- /dev/null +++ b/contrib/python/beniget/beniget/version.py @@ -0,0 +1 @@ +__version__ = '0.4.2.post1' diff --git a/contrib/python/beniget/ya.make b/contrib/python/beniget/ya.make index 156d734eaf..31c9e1c147 100644 --- a/contrib/python/beniget/ya.make +++ b/contrib/python/beniget/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(0.4.1) +VERSION(0.4.2.post1) LICENSE(BSD-3-Clause) @@ -15,7 +15,10 @@ NO_LINT() PY_SRCS( TOP_LEVEL beniget/__init__.py + beniget/__main__.py beniget/beniget.py + beniget/ordered_set.py + beniget/version.py ) RESOURCE_FILES( diff --git a/yt/python/yt/yson/__init__.py b/yt/python/yt/yson/__init__.py index c20ae5ea52..e9a303b4f8 100644 --- a/yt/python/yt/yson/__init__.py +++ b/yt/python/yt/yson/__init__.py @@ -22,7 +22,7 @@ Examples: >>> yson.dumps(True) '"true"' ->>> number = yson.YsonInteger(10) +>>> number = yson.YsonInt64(10) >>> number.attributes["my_attr"] = "hello" >>> yson.dumps(number) '<"attr"="hello">10' |