diff --git a/docs/source/_static/img/python_scopes.png b/docs/source/_static/img/python_scopes.png
index d5ad158f1..0c1b0266e 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 5865e27de..a1c04911e 100644
--- a/docs/source/_static/img/python_scopes.svg
+++ b/docs/source/_static/img/python_scopes.svg
@@ -1,6 +1,4 @@
-
-
diff --git a/docs/source/metadata.rst b/docs/source/metadata.rst
index f6c9c0784..0db0e7f31 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 477a631fb..01e2514b6 100644
--- a/libcst/metadata/__init__.py
+++ b/libcst/metadata/__init__.py
@@ -33,6 +33,7 @@
Assignments,
BaseAssignment,
BuiltinAssignment,
+ BuiltinScope,
ClassScope,
ComprehensionScope,
FunctionScope,
@@ -60,6 +61,7 @@
"BaseAssignment",
"Assignment",
"BuiltinAssignment",
+ "BuiltinScope",
"Access",
"Scope",
"GlobalScope",
diff --git a/libcst/metadata/scope_provider.py b/libcst/metadata/scope_provider.py
index 77ceafd17..919810ea5 100644
--- a/libcst/metadata/scope_provider.py
+++ b/libcst/metadata/scope_provider.py
@@ -338,13 +338,12 @@ def find_qualified_name_for_non_import(
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 @@ def accesses(self) -> Accesses:
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 27a8f495c..59a20aec7 100644
--- a/libcst/metadata/tests/test_scope_provider.py
+++ b/libcst/metadata/tests/test_scope_provider.py
@@ -13,6 +13,8 @@
from libcst.metadata import MetadataWrapper
from libcst.metadata.scope_provider import (
Assignment,
+ BuiltinAssignment,
+ BuiltinScope,
ClassScope,
ComprehensionScope,
FunctionScope,
@@ -144,6 +146,11 @@ def fn():
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 @@ def test_cast(self) -> None:
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)