diff --git a/src/python/pants/backend/codegen/protobuf/python/python_protobuf_module_mapper.py b/src/python/pants/backend/codegen/protobuf/python/python_protobuf_module_mapper.py new file mode 100644 index 00000000000..15e7541a40a --- /dev/null +++ b/src/python/pants/backend/codegen/protobuf/python/python_protobuf_module_mapper.py @@ -0,0 +1,69 @@ +# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from typing import Dict, Set, Tuple + +from pants.backend.codegen.protobuf.target_types import ProtobufGrpcToggle, ProtobufSources +from pants.backend.python.dependency_inference.module_mapper import ( + FirstPartyPythonMappingImpl, + FirstPartyPythonMappingImplMarker, +) +from pants.base.specs import AddressSpecs, DescendantAddresses +from pants.core.util_rules.stripped_source_files import StrippedSourceFileNames +from pants.engine.addresses import Address +from pants.engine.rules import Get, MultiGet, collect_rules, rule +from pants.engine.target import SourcesPathsRequest, Target, Targets +from pants.engine.unions import UnionRule +from pants.util.logging import LogLevel + + +def proto_path_to_py_module(stripped_path: str, *, suffix: str) -> str: + return stripped_path.replace(".proto", suffix).replace("/", ".") + + +# This is only used to register our implementation with the plugin hook via unions. +class PythonProtobufMappingMarker(FirstPartyPythonMappingImplMarker): + pass + + +@rule(desc="Creating map of Protobuf targets to generated Python modules", level=LogLevel.DEBUG) +async def map_protobuf_to_python_modules( + _: PythonProtobufMappingMarker, +) -> FirstPartyPythonMappingImpl: + all_expanded_targets = await Get(Targets, AddressSpecs([DescendantAddresses("")])) + protobuf_targets = tuple(tgt for tgt in all_expanded_targets if tgt.has_field(ProtobufSources)) + stripped_sources_per_target = await MultiGet( + Get(StrippedSourceFileNames, SourcesPathsRequest(tgt[ProtobufSources])) + for tgt in protobuf_targets + ) + + modules_to_addresses: Dict[str, Tuple[Address]] = {} + modules_with_multiple_owners: Set[str] = set() + + def add_module(module: str, tgt: Target) -> None: + if module in modules_to_addresses: + modules_with_multiple_owners.add(module) + else: + modules_to_addresses[module] = (tgt.address,) + + for tgt, stripped_sources in zip(protobuf_targets, stripped_sources_per_target): + for stripped_f in stripped_sources: + # NB: We don't consider the MyPy plugin, which generates `_pb2.pyi`. The stubs end up + # sharing the same module as the implementation `_pb2.py`. Because both generated files + # come from the same original Protobuf target, we're covered. + add_module(proto_path_to_py_module(stripped_f, suffix="_pb2"), tgt) + if tgt.get(ProtobufGrpcToggle).value: + add_module(proto_path_to_py_module(stripped_f, suffix="_pb2_grpc"), tgt) + + # Remove modules with ambiguous owners. + for ambiguous_module in modules_with_multiple_owners: + modules_to_addresses.pop(ambiguous_module) + + return FirstPartyPythonMappingImpl(modules_to_addresses) + + +def rules(): + return ( + *collect_rules(), + UnionRule(FirstPartyPythonMappingImplMarker, PythonProtobufMappingMarker), + ) diff --git a/src/python/pants/backend/codegen/protobuf/python/python_protobuf_module_mapper_test.py b/src/python/pants/backend/codegen/protobuf/python/python_protobuf_module_mapper_test.py new file mode 100644 index 00000000000..582230f79ec --- /dev/null +++ b/src/python/pants/backend/codegen/protobuf/python/python_protobuf_module_mapper_test.py @@ -0,0 +1,58 @@ +# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +import pytest + +from pants.backend.codegen.protobuf.python import additional_fields, python_protobuf_module_mapper +from pants.backend.codegen.protobuf.python.python_protobuf_module_mapper import ( + PythonProtobufMappingMarker, +) +from pants.backend.codegen.protobuf.target_types import ProtobufLibrary +from pants.backend.python.dependency_inference.module_mapper import FirstPartyPythonMappingImpl +from pants.core.util_rules import stripped_source_files +from pants.engine.addresses import Address +from pants.testutil.rule_runner import QueryRule, RuleRunner + + +@pytest.fixture +def rule_runner() -> RuleRunner: + return RuleRunner( + rules=[ + *additional_fields.rules(), + *stripped_source_files.rules(), + *python_protobuf_module_mapper.rules(), + QueryRule(FirstPartyPythonMappingImpl, [PythonProtobufMappingMarker]), + ], + target_types=[ProtobufLibrary], + ) + + +def test_map_first_party_modules_to_addresses(rule_runner: RuleRunner) -> None: + rule_runner.set_options(["--source-root-patterns=['root1', 'root2', 'root3']"]) + + # Two proto files belonging to the same target. We should use two file addresses. + rule_runner.create_files("root1/protos", ["f1.proto", "f2.proto"]) + rule_runner.add_to_build_file("root1/protos", "protobuf_library()") + + # These protos would result in the same module name, so neither should be used. + rule_runner.create_file("root1/two_owners/f.proto") + rule_runner.add_to_build_file("root1/two_owners", "protobuf_library()") + rule_runner.create_file("root2/two_owners/f.proto") + rule_runner.add_to_build_file("root2/two_owners", "protobuf_library()") + + # A file with grpc. This also uses the `python_source_root` mechanism, which should be + # irrelevant to the module mapping because we strip source roots. + rule_runner.create_file("root1/tests/f.proto") + rule_runner.add_to_build_file( + "root1/tests", "protobuf_library(grpc=True, python_source_root='root3')" + ) + + result = rule_runner.request(FirstPartyPythonMappingImpl, [PythonProtobufMappingMarker()]) + assert result == FirstPartyPythonMappingImpl( + { + "protos.f1_pb2": (Address("root1/protos", relative_file_path="f1.proto"),), + "protos.f2_pb2": (Address("root1/protos", relative_file_path="f2.proto"),), + "tests.f_pb2": (Address("root1/tests", relative_file_path="f.proto"),), + "tests.f_pb2_grpc": (Address("root1/tests", relative_file_path="f.proto"),), + } + ) diff --git a/src/python/pants/backend/codegen/protobuf/python/register.py b/src/python/pants/backend/codegen/protobuf/python/register.py index c602e65e91a..0f5251bf5fb 100644 --- a/src/python/pants/backend/codegen/protobuf/python/register.py +++ b/src/python/pants/backend/codegen/protobuf/python/register.py @@ -7,7 +7,11 @@ """ from pants.backend.codegen import export_codegen_goal -from pants.backend.codegen.protobuf.python import additional_fields, python_protobuf_subsystem +from pants.backend.codegen.protobuf.python import ( + additional_fields, + python_protobuf_module_mapper, + python_protobuf_subsystem, +) from pants.backend.codegen.protobuf.python.rules import rules as python_rules from pants.backend.codegen.protobuf.target_types import ProtobufLibrary @@ -17,6 +21,7 @@ def rules(): *additional_fields.rules(), *python_protobuf_subsystem.rules(), *python_rules(), + *python_protobuf_module_mapper.rules(), *export_codegen_goal.rules(), ] diff --git a/src/python/pants/backend/codegen/protobuf/python/rules.py b/src/python/pants/backend/codegen/protobuf/python/rules.py index 25ae67a47d6..163449b3839 100644 --- a/src/python/pants/backend/codegen/protobuf/python/rules.py +++ b/src/python/pants/backend/codegen/protobuf/python/rules.py @@ -7,7 +7,7 @@ from pants.backend.codegen.protobuf.python.additional_fields import PythonSourceRootField from pants.backend.codegen.protobuf.python.grpc_python_plugin import GrpcPythonPlugin from pants.backend.codegen.protobuf.python.python_protobuf_subsystem import PythonProtobufSubsystem -from pants.backend.codegen.protobuf.target_types import ProtobufGrcpToggle, ProtobufSources +from pants.backend.codegen.protobuf.target_types import ProtobufGrpcToggle, ProtobufSources from pants.backend.python.target_types import PythonSources from pants.backend.python.util_rules import extract_pex, pex from pants.backend.python.util_rules.extract_pex import ExtractedPexDistributions @@ -119,7 +119,7 @@ async def generate_python_from_protobuf( ExternalToolRequest, grpc_python_plugin.get_request(Platform.current), ) - if request.protocol_target.get(ProtobufGrcpToggle).value + if request.protocol_target.get(ProtobufGrpcToggle).value else None ) diff --git a/src/python/pants/backend/codegen/protobuf/target_types.py b/src/python/pants/backend/codegen/protobuf/target_types.py index 2731fd957b5..624445f6dae 100644 --- a/src/python/pants/backend/codegen/protobuf/target_types.py +++ b/src/python/pants/backend/codegen/protobuf/target_types.py @@ -15,7 +15,7 @@ class ProtobufSources(Sources): expected_file_extensions = (".proto",) -class ProtobufGrcpToggle(BoolField): +class ProtobufGrpcToggle(BoolField): """Whether to generate gRPC code or not.""" alias = "grpc" @@ -29,4 +29,4 @@ class ProtobufLibrary(Target): """ alias = "protobuf_library" - core_fields = (*COMMON_TARGET_FIELDS, ProtobufDependencies, ProtobufSources, ProtobufGrcpToggle) + core_fields = (*COMMON_TARGET_FIELDS, ProtobufDependencies, ProtobufSources, ProtobufGrpcToggle) diff --git a/src/python/pants/backend/python/dependency_inference/module_mapper.py b/src/python/pants/backend/python/dependency_inference/module_mapper.py index 9f8e4bf3499..87ed9f45f89 100644 --- a/src/python/pants/backend/python/dependency_inference/module_mapper.py +++ b/src/python/pants/backend/python/dependency_inference/module_mapper.py @@ -17,6 +17,7 @@ from pants.engine.collection import Collection from pants.engine.rules import Get, MultiGet, collect_rules, rule from pants.engine.target import SourcesPathsRequest, Targets +from pants.engine.unions import UnionMembership, UnionRule, union from pants.util.frozendict import FrozenDict from pants.util.logging import LogLevel @@ -33,50 +34,101 @@ def create_from_stripped_path(cls, path: PurePath) -> "PythonModule": return cls(module_name_with_slashes.as_posix().replace("/", ".")) -@dataclass(frozen=True) -class FirstPartyModuleToAddressMapping: - """A mapping of module names to owning addresses. +# ----------------------------------------------------------------------------------------------- +# First-party module mapping +# ----------------------------------------------------------------------------------------------- + + +class FirstPartyPythonMappingImpl(FrozenDict[str, Tuple[Address, ...]]): + """A mapping of module names to owning addresses that a specific implementation adds for Python + import dependency inference. + + For almost every implementation, there should only be one address per module to avoid ambiguity. + However, the built-in implementation allows for 2 addresses when `.pyi` type stubs are used. + """ + + +@union +class FirstPartyPythonMappingImplMarker: + """An entry point for a specific implementation of mapping module names to owning targets for + Python import dependency inference. - All mapped addresses will be file addresses, aka generated subtargets. That is, each target - will own no more than one single source file. + All implementations will be merged together. Any conflicting modules will be removed due to + ambiguity. - If there are >1 original owning targets that refer to the same module—such as `//:a` and `//:b` both owning module - `foo`—then we will not add any of the targets to the mapping because there is ambiguity. (We make an exception if - one target is an implementation (.py file) and the other is a type stub (.pyi file). + The addresses should all be file addresses, rather than BUILD addresses. """ - # The mapping should either have 1 or 2 addresses per module, depending on if there is a type - # stub. - mapping: FrozenDict[str, Tuple[Address, ...]] + +class FirstPartyPythonModuleMapping(FrozenDict[str, Tuple[Address, ...]]): + """A merged mapping of module names to owning addresses. + + This mapping may have been constructed from multiple distinct implementations, e.g. + implementations for each codegen backends. + """ def addresses_for_module(self, module: str) -> Tuple[Address, ...]: - targets = self.mapping.get(module) - if targets: - return targets + addresses = self.get(module) + if addresses: + return addresses # If the module is not found, try the parent, if any. This is to accommodate `from` # imports, where we don't care about the specific symbol, but only the module. For example, # with `from my_project.app import App`, we only care about the `my_project.app` part. # - # We do not look past the direct parent, as this could cause multiple ambiguous owners to be resolved. This - # contrasts with the third-party module mapping, which will try every ancestor. + # We do not look past the direct parent, as this could cause multiple ambiguous owners to + # be resolved. This contrasts with the third-party module mapping, which will try every + # ancestor. if "." not in module: return () parent_module = module.rsplit(".", maxsplit=1)[0] - return self.mapping.get(parent_module, ()) + return self.get(parent_module, ()) + + +@rule(level=LogLevel.DEBUG) +async def merge_first_party_module_mappings( + union_membership: UnionMembership, +) -> FirstPartyPythonModuleMapping: + all_mappings = await MultiGet( + Get( + FirstPartyPythonMappingImpl, + FirstPartyPythonMappingImplMarker, + marker_cls(), + ) + for marker_cls in union_membership.get(FirstPartyPythonMappingImplMarker) + ) + modules_to_addresses: Dict[str, Tuple[Address, ...]] = {} + modules_with_multiple_implementations: Set[str] = set() + for mapping in all_mappings: + for module, addresses in mapping.items(): + if module in modules_to_addresses: + modules_with_multiple_implementations.add(module) + else: + modules_to_addresses[module] = addresses + for module in modules_with_multiple_implementations: + modules_to_addresses.pop(module) + return FirstPartyPythonModuleMapping(sorted(modules_to_addresses.items())) -@rule(desc="Creating map of first party targets to Python modules", level=LogLevel.DEBUG) -async def map_first_party_modules_to_addresses() -> FirstPartyModuleToAddressMapping: +# This is only used to register our implementation with the plugin hook via unions. Note that we +# implement this like any other plugin implementation so that we can run them all in parallel. +class FirstPartyPythonTargetsMappingMarker(FirstPartyPythonMappingImplMarker): + pass + + +@rule(desc="Creating map of first party Python targets to Python modules", level=LogLevel.DEBUG) +async def map_first_party_python_targets_to_modules( + _: FirstPartyPythonTargetsMappingMarker, +) -> FirstPartyPythonMappingImpl: all_expanded_targets = await Get(Targets, AddressSpecs([DescendantAddresses("")])) - candidate_targets = tuple(tgt for tgt in all_expanded_targets if tgt.has_field(PythonSources)) + python_targets = tuple(tgt for tgt in all_expanded_targets if tgt.has_field(PythonSources)) stripped_sources_per_target = await MultiGet( Get(StrippedSourceFileNames, SourcesPathsRequest(tgt[PythonSources])) - for tgt in candidate_targets + for tgt in python_targets ) modules_to_addresses: DefaultDict[str, List[Address]] = defaultdict(list) modules_with_multiple_implementations: Set[str] = set() - for tgt, stripped_sources in zip(candidate_targets, stripped_sources_per_target): + for tgt, stripped_sources in zip(python_targets, stripped_sources_per_target): for stripped_f in stripped_sources: module = PythonModule.create_from_stripped_path(PurePath(stripped_f)).module if module in modules_to_addresses: @@ -96,24 +148,22 @@ async def map_first_party_modules_to_addresses() -> FirstPartyModuleToAddressMap # Remove modules with ambiguous owners. for module in modules_with_multiple_implementations: modules_to_addresses.pop(module) - return FirstPartyModuleToAddressMapping( - FrozenDict( - { - module: tuple(sorted(addresses)) - for module, addresses in sorted(modules_to_addresses.items()) - } - ) + + return FirstPartyPythonMappingImpl( + {k: tuple(sorted(v)) for k, v in modules_to_addresses.items()} ) -@dataclass(frozen=True) -class ThirdPartyModuleToAddressMapping: - mapping: FrozenDict[str, Address] +# ----------------------------------------------------------------------------------------------- +# Third party module mapping +# ----------------------------------------------------------------------------------------------- + +class ThirdPartyPythonModuleMapping(FrozenDict[str, Address]): def address_for_module(self, module: str) -> Optional[Address]: - target = self.mapping.get(module) - if target is not None: - return target + address = self.get(module) + if address is not None: + return address # If the module is not found, recursively try the ancestor modules, if any. For example, # pants.task.task.Task -> pants.task.task -> pants.task -> pants if "." not in module: @@ -123,7 +173,7 @@ def address_for_module(self, module: str) -> Optional[Address]: @rule(desc="Creating map of third party targets to Python modules", level=LogLevel.DEBUG) -async def map_third_party_modules_to_addresses() -> ThirdPartyModuleToAddressMapping: +async def map_third_party_modules_to_addresses() -> ThirdPartyPythonModuleMapping: all_targets = await Get(Targets, AddressSpecs([DescendantAddresses("")])) modules_to_addresses: Dict[str, Address] = {} modules_with_multiple_owners: Set[str] = set() @@ -144,7 +194,12 @@ async def map_third_party_modules_to_addresses() -> ThirdPartyModuleToAddressMap # Remove modules with ambiguous owners. for module in modules_with_multiple_owners: modules_to_addresses.pop(module) - return ThirdPartyModuleToAddressMapping(FrozenDict(sorted(modules_to_addresses.items()))) + return ThirdPartyPythonModuleMapping(sorted(modules_to_addresses.items())) + + +# ----------------------------------------------------------------------------------------------- +# module -> owners +# ----------------------------------------------------------------------------------------------- class PythonModuleOwners(Collection[Address]): @@ -158,15 +213,16 @@ class PythonModuleOwners(Collection[Address]): @rule async def map_module_to_address( module: PythonModule, - first_party_mapping: FirstPartyModuleToAddressMapping, - third_party_mapping: ThirdPartyModuleToAddressMapping, + first_party_mapping: FirstPartyPythonModuleMapping, + third_party_mapping: ThirdPartyPythonModuleMapping, ) -> PythonModuleOwners: third_party_address = third_party_mapping.address_for_module(module.module) first_party_addresses = first_party_mapping.addresses_for_module(module.module) - # It's possible for a user to write type stubs (`.pyi` files) for their third-party dependencies. We check if that - # happened, but we're strict in validating that there is only a single third party address and a single first-party - # address referring to a `.pyi` file; otherwise, we have ambiguous implementations, so no-op. + # It's possible for a user to write type stubs (`.pyi` files) for their third-party + # dependencies. We check if that happened, but we're strict in validating that there is only a + # single third party address and a single first-party address referring to a `.pyi` file; + # otherwise, we have ambiguous implementations, so no-op. third_party_resolved_only = third_party_address and not first_party_addresses third_party_resolved_with_type_stub = ( third_party_address @@ -189,4 +245,7 @@ async def map_module_to_address( def rules(): - return collect_rules() + return ( + *collect_rules(), + UnionRule(FirstPartyPythonMappingImplMarker, FirstPartyPythonTargetsMappingMarker), + ) diff --git a/src/python/pants/backend/python/dependency_inference/module_mapper_test.py b/src/python/pants/backend/python/dependency_inference/module_mapper_test.py index 7e4fbc10d7d..b7a9c65b8cb 100644 --- a/src/python/pants/backend/python/dependency_inference/module_mapper_test.py +++ b/src/python/pants/backend/python/dependency_inference/module_mapper_test.py @@ -7,18 +7,19 @@ import pytest +from pants.backend.codegen.protobuf.python import python_protobuf_module_mapper +from pants.backend.codegen.protobuf.target_types import ProtobufLibrary from pants.backend.python.dependency_inference.module_mapper import ( - FirstPartyModuleToAddressMapping, + FirstPartyPythonModuleMapping, PythonModule, PythonModuleOwners, - ThirdPartyModuleToAddressMapping, + ThirdPartyPythonModuleMapping, ) from pants.backend.python.dependency_inference.module_mapper import rules as module_mapper_rules from pants.backend.python.target_types import PythonLibrary, PythonRequirementLibrary from pants.core.util_rules import stripped_source_files from pants.engine.addresses import Address from pants.testutil.rule_runner import QueryRule, RuleRunner -from pants.util.frozendict import FrozenDict @pytest.mark.parametrize( @@ -40,8 +41,8 @@ def test_create_module_from_path(stripped_path: PurePath, expected: str) -> None def test_first_party_modules_mapping() -> None: util_addr = Address("src/python/util", relative_file_path="strutil.py") test_addr = Address("tests/python/project_test", relative_file_path="test.py") - mapping = FirstPartyModuleToAddressMapping( - FrozenDict({"util.strutil": (util_addr,), "project_test.test": (test_addr,)}) + mapping = FirstPartyPythonModuleMapping( + {"util.strutil": (util_addr,), "project_test.test": (test_addr,)} ) assert mapping.addresses_for_module("util.strutil") == (util_addr,) assert mapping.addresses_for_module("util.strutil.ensure_text") == (util_addr,) @@ -56,9 +57,7 @@ def test_first_party_modules_mapping() -> None: def test_third_party_modules_mapping() -> None: colors_addr = Address("", target_name="ansicolors") pants_addr = Address("", target_name="pantsbuild") - mapping = ThirdPartyModuleToAddressMapping( - FrozenDict({"colors": colors_addr, "pants": pants_addr}) - ) + mapping = ThirdPartyPythonModuleMapping({"colors": colors_addr, "pants": pants_addr}) assert mapping.address_for_module("colors") == colors_addr assert mapping.address_for_module("colors.red") == colors_addr assert mapping.address_for_module("pants") == pants_addr @@ -73,11 +72,12 @@ def rule_runner() -> RuleRunner: rules=[ *stripped_source_files.rules(), *module_mapper_rules(), - QueryRule(FirstPartyModuleToAddressMapping, ()), - QueryRule(ThirdPartyModuleToAddressMapping, ()), - QueryRule(PythonModuleOwners, (PythonModule,)), + *python_protobuf_module_mapper.rules(), + QueryRule(FirstPartyPythonModuleMapping, []), + QueryRule(ThirdPartyPythonModuleMapping, []), + QueryRule(PythonModuleOwners, [PythonModule]), ], - target_types=[PythonLibrary, PythonRequirementLibrary], + target_types=[PythonLibrary, PythonRequirementLibrary, ProtobufLibrary], ) @@ -85,24 +85,41 @@ def test_map_first_party_modules_to_addresses(rule_runner: RuleRunner) -> None: rule_runner.set_options( ["--source-root-patterns=['src/python', 'tests/python', 'build-support']"] ) + # Two modules belonging to the same target. We should generate subtargets for each file. rule_runner.create_files("src/python/project/util", ["dirutil.py", "tarutil.py"]) rule_runner.add_to_build_file("src/python/project/util", "python_library()") + # A module with two owners, meaning that neither should be resolved. rule_runner.create_file("src/python/two_owners.py") rule_runner.add_to_build_file("src/python", "python_library()") rule_runner.create_file("build-support/two_owners.py") rule_runner.add_to_build_file("build-support", "python_library()") + # A package module. Because there's only one source file belonging to the target, we should # not generate subtargets. rule_runner.create_file("tests/python/project_test/demo_test/__init__.py") rule_runner.add_to_build_file("tests/python/project_test/demo_test", "python_library()") + # A module with both an implementation and a type stub. rule_runner.create_files("src/python/stubs", ["stub.py", "stub.pyi"]) rule_runner.add_to_build_file("src/python/stubs", "python_library()") - result = rule_runner.request(FirstPartyModuleToAddressMapping, []) - assert result.mapping == FrozenDict( + # Check that plugin mappings work. Note that we duplicate one of the files with a normal + # python_library(), which means neither the Protobuf nor Python targets should be used. + rule_runner.create_files("src/python/protos", ["f1.proto", "f2.proto", "f2_pb2.py"]) + rule_runner.add_to_build_file( + "src/python/protos", + dedent( + """\ + protobuf_library(name='protos') + python_library(name='py') + """ + ), + ) + + result = rule_runner.request(FirstPartyPythonModuleMapping, []) + assert result == FirstPartyPythonModuleMapping( { "project.util.dirutil": ( Address("src/python/project/util", relative_file_path="dirutil.py"), @@ -113,6 +130,9 @@ def test_map_first_party_modules_to_addresses(rule_runner: RuleRunner) -> None: "project_test.demo_test": ( Address("tests/python/project_test/demo_test", relative_file_path="__init__.py"), ), + "protos.f1_pb2": ( + Address("src/python/protos", relative_file_path="f1.proto", target_name="protos"), + ), "stubs.stub": ( Address("src/python/stubs", relative_file_path="stub.py"), Address("src/python/stubs", relative_file_path="stub.pyi"), @@ -151,8 +171,8 @@ def test_map_third_party_modules_to_addresses(rule_runner: RuleRunner) -> None: """ ), ) - result = rule_runner.request(ThirdPartyModuleToAddressMapping, []) - assert result.mapping == FrozenDict( + result = rule_runner.request(ThirdPartyPythonModuleMapping, []) + assert result == ThirdPartyPythonModuleMapping( { "colors": Address("3rdparty/python", target_name="ansicolors"), "local_dist": Address("3rdparty/python", target_name="direct_references"),