Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 8 additions & 6 deletions libcst/_metadata_dependent.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,19 @@ def get_inherited_dependencies(cls) -> Collection["ProviderT"]:

Recursively searches the MRO of the subclass for metadata dependencies.
"""
try:
# pyre-fixme[16]: use a hidden attribute to cache the property
return cls._INHERITED_METADATA_DEPENDENCIES_CACHE
except AttributeError:
# Only consult this class's own cache entry: a plain attribute lookup
# would walk the MRO and return a base class's cached value, so a bare
# base class resolved first would hide the subclass's dependencies.
cache = cls.__dict__.get("_INHERITED_METADATA_DEPENDENCIES_CACHE")
if cache is None:
dependencies = set()
for c in inspect.getmro(cls):
if issubclass(c, MetadataDependent):
dependencies.update(c.METADATA_DEPENDENCIES)
cache = frozenset(dependencies)
# pyre-fixme[16]: use a hidden attribute to cache the property
cls._INHERITED_METADATA_DEPENDENCIES_CACHE = frozenset(dependencies)
return cls._INHERITED_METADATA_DEPENDENCIES_CACHE
cls._INHERITED_METADATA_DEPENDENCIES_CACHE = cache
return cache

@contextmanager
def resolve(self, wrapper: "MetadataWrapper") -> Iterator[None]:
Expand Down
22 changes: 22 additions & 0 deletions libcst/metadata/tests/test_metadata_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,28 @@ def visit_Pass(self, node: cst.Pass) -> None:
# Check each visitor is called once
mock.visited_simple.assert_called_once()

def test_bare_base_class_does_not_poison_inherited_metadata(self) -> None:
"""
Tests that resolving a bare base class first does not hide the
dependencies declared by a subclass.
"""
test_runner = self

class SimpleProvider(VisitorMetadataProvider[int]):
def visit_Pass(self, node: cst.Pass) -> None:
self.set_metadata(node, 1)

class DependentVisitor(CSTTransformer):
METADATA_DEPENDENCIES = (SimpleProvider,)

def visit_Pass(self, node: cst.Pass) -> None:
test_runner.assertEqual(self.get_metadata(SimpleProvider, node), 1)

wrapper = MetadataWrapper(parse_module("pass"))
# Resolving the bare base class caches an empty dependency set on it.
wrapper.visit(CSTTransformer())
wrapper.visit(DependentVisitor())

def test_provider_inherited_metadata(self) -> None:
"""
Tests that providers inherit access to metadata declared by their base
Expand Down