diff --git a/docs/source/_static/img/python_scopes.png b/docs/source/_static/img/python_scopes.png index d5ad158f..0c1b0266 100644 Binary files a/docs/source/_static/img/python_scopes.png and b/docs/source/_static/img/python_scopes.png differ diff --git a/docs/source/_static/img/python_scopes.svg b/docs/source/_static/img/python_scopes.svg index 5865e27d..a1c04911 100644 --- a/docs/source/_static/img/python_scopes.svg +++ b/docs/source/_static/img/python_scopes.svg @@ -1,6 +1,4 @@ - - + inkscape:version="1.0.2 (e86c8708, 2021-01-15)" + sodipodi:docname="drawing.svg" + inkscape:export-filename="/Users/lpetre/Desktop/rect846-0.png" + inkscape:export-xdpi="191.53999" + inkscape:export-ydpi="191.53999"> - - - + @@ -63,223 +58,440 @@ - - - - - - - global scope - - - - class scope - - - - function scope - - - - - comprehension scope - - - - ITERATIONS = 10class Cls: class_attribute = 20 def fn(): for i in range(ITERATIONS): ... return [ i for i in range(10) ]Cls().fn() + id="layer1"> + + + + + builtin scope + + + + class range(stop) ... + + + + + + + global scope + + + + ITERATIONS = 10Cls().fn() + + + + + + + class scope + + + + class Cls: class_attribute = 20 + + + + + + + function scope + + + + def fn(): for i in range(ITERATIONS): ... + + + + + + + comprehension scope + + + + return [ i for i in range(10) ] + + diff --git a/docs/source/metadata.rst b/docs/source/metadata.rst index f6c9c078..0db0e7f3 100644 --- a/docs/source/metadata.rst +++ b/docs/source/metadata.rst @@ -40,8 +40,8 @@ The wrapper provides a :func:`~libcst.metadata.MetadataWrapper.resolve` function .. autoclass:: libcst.metadata.MetadataWrapper :special-members: __init__ -If you're working with visitors, which extend :class:`~libcst.MetadataDependent`, -metadata dependencies will be automatically computed when visited by a +If you're working with visitors, which extend :class:`~libcst.MetadataDependent`, +metadata dependencies will be automatically computed when visited by a :class:`~libcst.metadata.MetadataWrapper` and are accessible through :func:`~libcst.MetadataDependent.get_metadata` @@ -134,14 +134,15 @@ New scopes are created for classes, functions, and comprehensions. Other block constructs like conditional statements, loops, and try…except don't create their own scope. -There are four different type of scope in Python: +There are five different type of scope in Python: +:class:`~libcst.metadata.BuiltinScope`, :class:`~libcst.metadata.GlobalScope`, :class:`~libcst.metadata.ClassScope`, :class:`~libcst.metadata.FunctionScope`, and :class:`~libcst.metadata.ComprehensionScope`. .. image:: _static/img/python_scopes.png - :alt: Diagram showing how the above 4 scopes are nested in each other + :alt: Diagram showing how the above 5 scopes are nested in each other :width: 400 :align: center @@ -175,6 +176,9 @@ assigned or accessed within. :no-undoc-members: :special-members: __contains__, __getitem__, __iter__ +.. autoclass:: libcst.metadata.BuiltinScope + :no-undoc-members: + .. autoclass:: libcst.metadata.GlobalScope :no-undoc-members: diff --git a/libcst/metadata/__init__.py b/libcst/metadata/__init__.py index 477a631f..01e2514b 100644 --- a/libcst/metadata/__init__.py +++ b/libcst/metadata/__init__.py @@ -33,6 +33,7 @@ from libcst.metadata.scope_provider import ( Assignments, BaseAssignment, BuiltinAssignment, + BuiltinScope, ClassScope, ComprehensionScope, FunctionScope, @@ -60,6 +61,7 @@ __all__ = [ "BaseAssignment", "Assignment", "BuiltinAssignment", + "BuiltinScope", "Access", "Scope", "GlobalScope", diff --git a/libcst/metadata/scope_provider.py b/libcst/metadata/scope_provider.py index 77ceafd1..919810ea 100644 --- a/libcst/metadata/scope_provider.py +++ b/libcst/metadata/scope_provider.py @@ -338,13 +338,12 @@ class _NameUtil: name_prefixes.append(scope.name) elif isinstance(scope, FunctionScope): name_prefixes.append(f"{scope.name}.") - elif isinstance(scope, GlobalScope): - break elif isinstance(scope, ComprehensionScope): name_prefixes.append("") - else: + elif not isinstance(scope, (GlobalScope, BuiltinScope)): raise Exception(f"Unexpected Scope: {scope}") - scope = scope.parent + + scope = scope.parent if scope.parent != scope else None parts = [*reversed(name_prefixes)] if remaining_name: @@ -536,27 +535,57 @@ class Scope(abc.ABC): return Accesses(self._accesses) +class BuiltinScope(Scope): + """ + A BuiltinScope represents python builtin declarations. See https://docs.python.org/3/library/builtins.html + """ + + def __init__(self, globals: Scope) -> None: + self.globals: Scope = globals # must be defined before Scope.__init__ is called + super().__init__(parent=self) + + def __contains__(self, name: str) -> bool: + return hasattr(builtins, name) + + def __getitem__(self, name: str) -> Set[BaseAssignment]: + if name in self._assignments: + return self._assignments[name] + if hasattr(builtins, name): + # note - we only see the builtin assignments during the deferred + # access resolution. unfortunately that means we have to create the + # assignment here, which can cause the set to mutate during iteration + self._assignments[name].add(BuiltinAssignment(name, self)) + return self._assignments[name] + return set() + + def record_assignment(self, name: str, node: cst.CSTNode) -> None: + raise NotImplementedError("assignments in builtin scope are not allowed") + + def record_global_overwrite(self, name: str) -> None: + raise NotImplementedError("global overwrite in builtin scope are not allowed") + + def record_nonlocal_overwrite(self, name: str) -> None: + raise NotImplementedError("declarations in builtin scope are not allowed") + + class GlobalScope(Scope): """ A GlobalScope is the scope of module. All module level assignments are recorded in GlobalScope. """ def __init__(self) -> None: - self.globals: Scope = self # must be defined before Scope.__init__ is called - super().__init__(parent=self) + super().__init__(parent=BuiltinScope(self)) def __contains__(self, name: str) -> bool: - return hasattr(builtins, name) or ( - name in self._assignments and len(self._assignments[name]) > 0 - ) + if name in self._assignments: + return len(self._assignments[name]) > 0 + return self.parent._contains_in_self_or_parent(name) def __getitem__(self, name: str) -> Set[BaseAssignment]: - if hasattr(builtins, name): - if not any( - isinstance(i, BuiltinAssignment) for i in self._assignments[name] - ): - self._assignments[name].add(BuiltinAssignment(name, self)) - return self._assignments[name] + if name in self._assignments: + return self._assignments[name] + else: + return self.parent._getitem_from_self_or_parent(name) def record_global_overwrite(self, name: str) -> None: pass diff --git a/libcst/metadata/tests/test_scope_provider.py b/libcst/metadata/tests/test_scope_provider.py index 27a8f495..59a20aec 100644 --- a/libcst/metadata/tests/test_scope_provider.py +++ b/libcst/metadata/tests/test_scope_provider.py @@ -13,6 +13,8 @@ from libcst import ensure_type from libcst.metadata import MetadataWrapper from libcst.metadata.scope_provider import ( Assignment, + BuiltinAssignment, + BuiltinScope, ClassScope, ComprehensionScope, FunctionScope, @@ -144,6 +146,11 @@ class ScopeProviderTest(UnitTest): self.assertEqual(len(scope_of_module[builtin]), 1) self.assertEqual(len(scope_of_module["something_not_a_builtin"]), 0) + scope_of_builtin = scope_of_module.parent + self.assertIsInstance(scope_of_builtin, BuiltinScope) + self.assertEqual(len(scope_of_builtin[builtin]), 1) + self.assertEqual(len(scope_of_builtin["something_not_a_builtin"]), 0) + func_body = ensure_type(m.body[0], cst.FunctionDef).body func_pass_statement = func_body.body[0] scope_of_func_statement = scopes[func_pass_statement] @@ -1687,3 +1694,70 @@ class ScopeProviderTest(UnitTest): cast("3rr0r", "") """ ) + + def test_builtin_scope(self) -> None: + m, scopes = get_scope_metadata_provider( + """ + a = pow(1, 2) + def foo(): + b = pow(2, 3) + """ + ) + scope_of_module = scopes[m] + self.assertIsInstance(scope_of_module, GlobalScope) + self.assertEqual(len(scope_of_module["pow"]), 1) + builtin_pow_assignment = list(scope_of_module["pow"])[0] + self.assertIsInstance(builtin_pow_assignment, BuiltinAssignment) + self.assertIsInstance(builtin_pow_assignment.scope, BuiltinScope) + + global_a_assignments = scope_of_module["a"] + self.assertEqual(len(global_a_assignments), 1) + a_assignment = list(global_a_assignments)[0] + self.assertIsInstance(a_assignment, Assignment) + + func_body = ensure_type(m.body[1], cst.FunctionDef).body + func_statement = func_body.body[0] + scope_of_func_statement = scopes[func_statement] + self.assertIsInstance(scope_of_func_statement, FunctionScope) + func_b_assignments = scope_of_func_statement["b"] + self.assertEqual(len(func_b_assignments), 1) + b_assignment = list(func_b_assignments)[0] + self.assertIsInstance(b_assignment, Assignment) + + builtin_pow_accesses = list(builtin_pow_assignment.references) + self.assertEqual(len(builtin_pow_accesses), 2) + + def test_override_builtin_scope(self) -> None: + m, scopes = get_scope_metadata_provider( + """ + def pow(x, y): + return x ** y + + a = pow(1, 2) + def foo(): + b = pow(2, 3) + """ + ) + scope_of_module = scopes[m] + self.assertIsInstance(scope_of_module, GlobalScope) + self.assertEqual(len(scope_of_module["pow"]), 1) + global_pow_assignment = list(scope_of_module["pow"])[0] + self.assertIsInstance(global_pow_assignment, Assignment) + self.assertIsInstance(global_pow_assignment.scope, GlobalScope) + + global_a_assignments = scope_of_module["a"] + self.assertEqual(len(global_a_assignments), 1) + a_assignment = list(global_a_assignments)[0] + self.assertIsInstance(a_assignment, Assignment) + + func_body = ensure_type(m.body[2], cst.FunctionDef).body + func_statement = func_body.body[0] + scope_of_func_statement = scopes[func_statement] + self.assertIsInstance(scope_of_func_statement, FunctionScope) + func_b_assignments = scope_of_func_statement["b"] + self.assertEqual(len(func_b_assignments), 1) + b_assignment = list(func_b_assignments)[0] + self.assertIsInstance(b_assignment, Assignment) + + global_pow_accesses = list(global_pow_assignment.references) + self.assertEqual(len(global_pow_accesses), 2)