Example #1
0
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"), ),
    })
Example #2
0
def test_file_dependencies(rule_runner: RuleRunner) -> None:
    # img_A -> files_A
    # img_A -> img_B -> files_B
    rule_runner.add_to_build_file(
        "src/a",
        dedent("""\
            docker_image(name="img_A", dependencies=[":files_A", "src/b:img_B"])
            files(name="files_A", sources=["files/**"])
            """),
    )
    rule_runner.add_to_build_file(
        "src/b",
        dedent("""\
            docker_image(name="img_B", dependencies=[":files_B"])
            files(name="files_B", sources=["files/**"])
            """),
    )
    rule_runner.create_files("src/a", ["Dockerfile"])
    rule_runner.create_files("src/a/files", ["a01", "a02"])
    rule_runner.create_files("src/b", ["Dockerfile"])
    rule_runner.create_files("src/b/files", ["b01", "b02"])

    # We want files_B in build context for img_B
    assert_build_context(
        rule_runner,
        Address("src/b", target_name="img_B"),
        expected_files=[
            "src/b/Dockerfile", "src/b/files/b01", "src/b/files/b02"
        ],
    )

    # We want files_A in build context for img_A, but not files_B
    assert_build_context(
        rule_runner,
        Address("src/a", target_name="img_A"),
        expected_files=[
            "src/a/Dockerfile", "src/a/files/a01", "src/a/files/a02"
        ],
    )

    # Mixed.
    rule_runner.add_to_build_file(
        "src/c",
        dedent("""\
            docker_image(name="img_C", dependencies=["src/a:files_A", "src/b:files_B"])
            """),
    )
    rule_runner.create_files("src/c", ["Dockerfile"])

    assert_build_context(
        rule_runner,
        Address("src/c", target_name="img_C"),
        expected_files=[
            "src/c/Dockerfile",
            "src/a/files/a01",
            "src/a/files/a02",
            "src/b/files/b01",
            "src/b/files/b02",
        ],
    )
Example #3
0
def test_inject_handler_dependency(rule_runner: RuleRunner) -> None:
    rule_runner.add_to_build_file(
        "",
        dedent("""\
            python_requirement_library(
                name='ansicolors',
                requirements=['ansicolors'],
                module_mapping={'ansicolors': ['colors']},
            )
            """),
    )
    rule_runner.create_files("project", ["app.py", "self.py"])
    rule_runner.add_to_build_file(
        "project",
        dedent("""\
            python_library()
            python_awslambda(name='first_party', handler='project.app:func', runtime='python3.7')
            python_awslambda(name='first_party_shorthand', handler='app.py:func', runtime='python3.7')
            python_awslambda(name='third_party', handler='colors:func', runtime='python3.7')
            python_awslambda(name='unrecognized', handler='who_knows.module:func', runtime='python3.7')
            python_awslambda(name='self', handler='self.py:func', runtime='python3.7')
            """),
    )

    def assert_injected(address: Address, *,
                        expected: Optional[Address]) -> None:
        tgt = rule_runner.get_target(address)
        injected = rule_runner.request(
            InjectedDependencies,
            [
                InjectPythonLambdaHandlerDependency(
                    tgt[PythonAwsLambdaDependencies])
            ],
        )
        assert injected == InjectedDependencies([expected] if expected else [])

    assert_injected(
        Address("project", target_name="first_party"),
        expected=Address("project", relative_file_path="app.py"),
    )
    assert_injected(
        Address("project", target_name="first_party_shorthand"),
        expected=Address("project", relative_file_path="app.py"),
    )
    assert_injected(
        Address("project", target_name="third_party"),
        expected=Address("", target_name="ansicolors"),
    )
    assert_injected(Address("project", target_name="unrecognized"),
                    expected=None)
    assert_injected(
        Address("project", target_name="self"),
        expected=Address("project", relative_file_path="self.py"),
    )

    # Test that we can turn off the injection.
    rule_runner.set_options(["--no-python-infer-entry-points"])
    assert_injected(Address("project", target_name="first_party"),
                    expected=None)
Example #4
0
def create_target(
    rule_runner: RuleRunner,
    *,
    parent_directory: str,
    files: List[str],
    target_cls: Type[Target] = PythonTarget,
) -> Target:
    rule_runner.create_files(parent_directory, files=files)
    address = Address(spec_path=parent_directory, target_name="target")
    return target_cls({Sources.alias: files}, address=address)
Example #5
0
def test_globs(rule_runner: RuleRunner) -> None:
    rule_runner.create_files("some/target", ["test1.py", "test2.py"])
    rule_runner.add_to_build_file("some/target",
                                  target="tgt(sources=['test*.py'])")
    assert_filedeps(
        rule_runner,
        targets=["some/target"],
        expected={"some/target/BUILD", "some/target/test*.py"},
        globs=True,
    )
Example #6
0
def mock_sources_field(
    rule_runner: RuleRunner,
    sources: TargetSources,
    *,
    include_sources: bool = True,
    sources_field_cls: Type[SourcesField] = SourcesField,
) -> SourcesField:
    sources_field = sources_field_cls(
        sources.source_files if include_sources else [],
        address=Address.parse(f"{sources.source_root}:lib"),
    )
    rule_runner.create_files(path=sources.source_root, files=sources.source_files)
    return sources_field
Example #7
0
def setup_target(
    rule_runner: RuleRunner,
    path: str,
    *,
    sources: Optional[List[str]] = None,
    dependencies: Optional[List[str]] = None,
) -> None:
    if sources:
        rule_runner.create_files(path, sources)
    rule_runner.add_to_build_file(
        path,
        f"tgt(sources={sources or []}, dependencies={dependencies or []})",
    )
Example #8
0
def test_invalid_binary(chroot_rule_runner: RuleRunner) -> None:
    chroot_rule_runner.create_files("src/python/invalid_binary", ["app1.py", "app2.py"])
    chroot_rule_runner.add_to_build_file(
        "src/python/invalid_binary",
        textwrap.dedent(
            """
            python_library(name='not_a_binary', sources=[])
            pex_binary(name='invalid_entrypoint_unowned1', entry_point='app1.py')
            pex_binary(name='invalid_entrypoint_unowned2', entry_point='invalid_binary.app2')
            python_distribution(
                name='invalid_bin1',
                provides=setup_py(
                    name='invalid_bin1', version='1.1.1'
                ).with_binaries(foo=':not_a_binary')
            )
            python_distribution(
                name='invalid_bin2',
                provides=setup_py(
                    name='invalid_bin2', version='1.1.1'
                ).with_binaries(foo=':invalid_entrypoint_unowned1')
            )
            python_distribution(
                name='invalid_bin3',
                provides=setup_py(
                    name='invalid_bin3', version='1.1.1'
                ).with_binaries(foo=':invalid_entrypoint_unowned2')
            )
            """
        ),
    )

    assert_chroot_error(
        chroot_rule_runner,
        Address("src/python/invalid_binary", target_name="invalid_bin1"),
        InvalidEntryPoint,
    )
    assert_chroot_error(
        chroot_rule_runner,
        Address("src/python/invalid_binary", target_name="invalid_bin2"),
        InvalidEntryPoint,
    )
    assert_chroot_error(
        chroot_rule_runner,
        Address("src/python/invalid_binary", target_name="invalid_bin3"),
        InvalidEntryPoint,
    )
def test_protobuf_mapping(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 stripped file 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()")

    result = rule_runner.request(ProtobufMapping, [])
    assert result == ProtobufMapping({
        "protos/f1.proto":
        Address("root1/protos", relative_file_path="f1.proto"),
        "protos/f2.proto":
        Address("root1/protos", relative_file_path="f2.proto"),
    })
Example #10
0
def test_files_out_of_tree(rule_runner: RuleRunner) -> None:
    # src/a:img_A -> res/static:files
    rule_runner.add_to_build_file(
        "src/a",
        dedent("""\
            docker_image(name="img_A", dependencies=["res/static:files"])
            """),
    )
    rule_runner.add_to_build_file(
        "res/static",
        dedent("""\
            files(name="files", sources=["!BUILD", "**/*"])
            """),
    )
    rule_runner.create_files("src/a", ["Dockerfile"])
    rule_runner.create_files("res/static", ["s01", "s02"])
    rule_runner.create_files("res/static/sub", ["s03"])

    assert_build_context(
        rule_runner,
        Address("src/a", target_name="img_A"),
        expected_files=[
            "src/a/Dockerfile",
            "res/static/s01",
            "res/static/s02",
            "res/static/sub/s03",
        ],
    )
Example #11
0
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()")

    # 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"),
            ),
            "project.util.tarutil": (
                Address("src/python/project/util", relative_file_path="tarutil.py"),
            ),
            "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"),
            ),
        }
    )
Example #12
0
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({
        "project.util.dirutil": (Address("src/python/project/util",
                                         relative_file_path="dirutil.py"), ),
        "project.util.tarutil": (Address("src/python/project/util",
                                         relative_file_path="tarutil.py"), ),
        "project_test.demo_test":
        (Address("tests/python/project_test/demo_test",
                 relative_file_path="__init__.py"), ),
        "stubs.stub": (
            Address("src/python/stubs", relative_file_path="stub.py"),
            Address("src/python/stubs", relative_file_path="stub.pyi"),
        ),
    })
Example #13
0
def test_infer_python_imports(caplog) -> None:
    rule_runner = RuleRunner(
        rules=[
            *import_rules(),
            *target_types_rules.rules(),
            QueryRule(InferredDependencies, [InferPythonImportDependencies]),
        ],
        target_types=[PythonSourcesGeneratorTarget, PythonRequirementTarget],
    )
    rule_runner.add_to_build_file(
        "3rdparty/python",
        dedent("""\
            python_requirement(
              name='Django',
              requirements=['Django==1.21'],
            )
            """),
    )

    # If there's a `.py` and `.pyi` file for the same module, we should infer a dependency on both.
    rule_runner.create_file("src/python/str_import/subdir/f.py")
    rule_runner.create_file("src/python/str_import/subdir/f.pyi")
    rule_runner.add_to_build_file("src/python/str_import/subdir",
                                  "python_sources()")

    rule_runner.create_file("src/python/util/dep.py")
    rule_runner.add_to_build_file("src/python/util", "python_sources()")

    rule_runner.create_file(
        "src/python/app.py",
        dedent("""\
            import django
            import unrecognized.module

            from util.dep import Demo
            from util import dep
            """),
    )
    rule_runner.create_file(
        "src/python/f2.py",
        dedent("""\
            import typing
            # Import from another file in the same target.
            from app import main

            # Dynamic string import.
            importlib.import_module('str_import.subdir.f')
            """),
    )
    rule_runner.add_to_build_file("src/python", "python_sources()")

    def run_dep_inference(
            address: Address,
            *,
            enable_string_imports: bool = False) -> InferredDependencies:
        args = [
            "--backend-packages=pants.backend.python",
            "--source-root-patterns=src/python"
        ]
        if enable_string_imports:
            args.append("--python-infer-string-imports")
        rule_runner.set_options(args,
                                env_inherit={"PATH", "PYENV_ROOT", "HOME"})
        target = rule_runner.get_target(address)
        return rule_runner.request(
            InferredDependencies,
            [InferPythonImportDependencies(target[PythonSourceField])])

    assert run_dep_inference(Address(
        "src/python", relative_file_path="app.py")) == InferredDependencies([
            Address("3rdparty/python", target_name="Django"),
            Address("src/python/util", relative_file_path="dep.py"),
        ], )

    addr = Address("src/python", relative_file_path="f2.py")
    assert run_dep_inference(addr) == InferredDependencies(
        [Address("src/python", relative_file_path="app.py")])
    assert run_dep_inference(
        addr, enable_string_imports=True) == InferredDependencies([
            Address("src/python", relative_file_path="app.py"),
            Address("src/python/str_import/subdir", relative_file_path="f.py"),
            Address("src/python/str_import/subdir",
                    relative_file_path="f.pyi"),
        ], )

    # Test handling of ambiguous imports. We should warn on the ambiguous dependency, but not warn
    # on the disambiguated one and should infer a dep.
    caplog.clear()
    rule_runner.create_files("src/python/ambiguous",
                             ["dep.py", "disambiguated_via_ignores.py"])
    rule_runner.create_file(
        "src/python/ambiguous/main.py",
        "import ambiguous.dep\nimport ambiguous.disambiguated_via_ignores\n",
    )
    rule_runner.add_to_build_file(
        "src/python/ambiguous",
        dedent("""\
            python_sources(name='dep1', sources=['dep.py', 'disambiguated_via_ignores.py'])
            python_sources(name='dep2', sources=['dep.py', 'disambiguated_via_ignores.py'])
            python_sources(
                name='main',
                sources=['main.py'],
                dependencies=['!./disambiguated_via_ignores.py:dep2'],
            )
            """),
    )
    assert run_dep_inference(
        Address("src/python/ambiguous",
                target_name="main",
                relative_file_path="main.py")) == InferredDependencies([
                    Address(
                        "src/python/ambiguous",
                        target_name="dep1",
                        relative_file_path="disambiguated_via_ignores.py",
                    )
                ], )
    assert len(caplog.records) == 1
    assert "The target src/python/ambiguous/main.py:main imports `ambiguous.dep`" in caplog.text
    assert "['src/python/ambiguous/dep.py:dep1', 'src/python/ambiguous/dep.py:dep2']" in caplog.text
    assert "disambiguated_via_ignores.py" not in caplog.text
Example #14
0
def test_inject_pex_binary_entry_point_dependency() -> None:
    rule_runner = RuleRunner(
        rules=[
            inject_pex_binary_entry_point_dependency,
            resolve_pex_entry_point,
            *import_rules(),
            QueryRule(InjectedDependencies,
                      [InjectPexBinaryEntryPointDependency]),
        ],
        target_types=[PexBinary, PythonRequirementLibrary, PythonLibrary],
    )
    rule_runner.add_to_build_file(
        "",
        dedent("""\
            python_requirement_library(
                name='ansicolors',
                requirements=['ansicolors'],
                module_mapping={'ansicolors': ['colors']},
            )
            """),
    )
    rule_runner.create_files("project", ["app.py", "self.py"])
    rule_runner.add_to_build_file(
        "project",
        dedent("""\
            python_library(sources=['app.py'])
            pex_binary(name='first_party', entry_point='project.app')
            pex_binary(name='first_party_func', entry_point='project.app:func')
            pex_binary(name='first_party_shorthand', entry_point='app.py:func')
            pex_binary(name='first_party_shorthand_func', entry_point='app.py:func')
            pex_binary(name='third_party', entry_point='colors')
            pex_binary(name='third_party_func', entry_point='colors:func')
            pex_binary(name='unrecognized', entry_point='who_knows.module')
            """),
    )

    def assert_injected(address: Address, *,
                        expected: Optional[Address]) -> None:
        tgt = rule_runner.get_target(address)
        injected = rule_runner.request(
            InjectedDependencies,
            [InjectPexBinaryEntryPointDependency(tgt[PexBinaryDependencies])],
        )
        assert injected == InjectedDependencies([expected] if expected else [])

    assert_injected(
        Address("project", target_name="first_party"),
        expected=Address("project", relative_file_path="app.py"),
    )
    assert_injected(
        Address("project", target_name="first_party_func"),
        expected=Address("project", relative_file_path="app.py"),
    )
    assert_injected(
        Address("project", target_name="first_party_shorthand"),
        expected=Address("project", relative_file_path="app.py"),
    )
    assert_injected(
        Address("project", target_name="first_party_shorthand_func"),
        expected=Address("project", relative_file_path="app.py"),
    )
    assert_injected(
        Address("project", target_name="third_party"),
        expected=Address("", target_name="ansicolors"),
    )
    assert_injected(
        Address("project", target_name="third_party_func"),
        expected=Address("", target_name="ansicolors"),
    )
    assert_injected(Address("project", target_name="unrecognized"),
                    expected=None)

    # Test that we can turn off the injection.
    rule_runner.set_options(["--no-python-infer-entry-points"])
    assert_injected(Address("project", target_name="first_party"),
                    expected=None)
Example #15
0
def test_map_module_to_address(rule_runner: RuleRunner) -> None:
    rule_runner.set_options(["--source-root-patterns=['source_root1', 'source_root2', '/']"])

    def assert_owners(
        module: str, *, expected: list[Address], expected_ambiguous: list[Address] | None = None
    ) -> None:
        owners = rule_runner.request(PythonModuleOwners, [PythonModule(module)])
        assert list(owners.unambiguous) == expected
        assert list(owners.ambiguous) == (expected_ambiguous or [])

    # First check that we can map 3rd-party modules without ambiguity.
    rule_runner.add_to_build_file(
        "3rdparty/python",
        dedent(
            """\
            python_requirement_library(
              name='ansicolors',
              requirements=['ansicolors==1.21'],
              module_mapping={'ansicolors': ['colors']},
            )
            """
        ),
    )
    assert_owners("colors.red", expected=[Address("3rdparty/python", target_name="ansicolors")])

    # Now test that we can handle first-party type stubs that go along with that third party
    # requirement. Note that `colors.pyi` is at the top-level of the source root so that it strips
    # to the module `colors`.
    rule_runner.create_file("source_root1/colors.pyi")
    rule_runner.add_to_build_file("source_root1", "python_library()")
    assert_owners(
        "colors.red",
        expected=[
            Address("3rdparty/python", target_name="ansicolors"),
            Address("source_root1", relative_file_path="colors.pyi"),
        ],
    )

    # But don't allow a first-party implementation with the same module name.
    Path(rule_runner.build_root, "source_root1/colors.pyi").unlink()
    rule_runner.create_file("source_root1/colors.py")
    assert_owners(
        "colors.red",
        expected=[],
        expected_ambiguous=[
            Address("3rdparty/python", target_name="ansicolors"),
            Address("source_root1", relative_file_path="colors.py"),
        ],
    )

    # Check a first party module using a module path.
    rule_runner.create_file("source_root1/project/app.py")
    rule_runner.create_file("source_root1/project/file2.py")
    rule_runner.add_to_build_file("source_root1/project", "python_library()")
    assert_owners(
        "project.app", expected=[Address("source_root1/project", relative_file_path="app.py")]
    )

    # Now check with a type stub.
    rule_runner.create_file("source_root1/project/app.pyi")
    assert_owners(
        "project.app",
        expected=[
            Address("source_root1/project", relative_file_path="app.py"),
            Address("source_root1/project", relative_file_path="app.pyi"),
        ],
    )

    # Check a package path
    rule_runner.create_file("source_root2/project/subdir/__init__.py")
    rule_runner.add_to_build_file("source_root2/project/subdir", "python_library()")
    assert_owners(
        "project.subdir",
        expected=[Address("source_root2/project/subdir", relative_file_path="__init__.py")],
    )

    # Test a module with no owner (stdlib). This also smoke tests that we can handle when
    # there is no parent module.
    assert_owners("typing", expected=[])

    # Test a module with a single owner with a top-level source root of ".". Also confirm we
    # can handle when the module includes a symbol (like a class name) at the end.
    rule_runner.create_file("script.py")
    rule_runner.add_to_build_file("", "python_library(name='script')")
    assert_owners(
        "script.Demo", expected=[Address("", relative_file_path="script.py", target_name="script")]
    )

    # Ambiguous modules should be recorded.
    rule_runner.create_files("source_root1/ambiguous", ["f1.py", "f2.py", "f3.py"])
    rule_runner.add_to_build_file(
        "source_root1/ambiguous",
        dedent(
            """\
            # Ambiguity purely within third-party deps.
            python_requirement_library(name='thirdparty1', requirements=['foo'])
            python_requirement_library(name='thirdparty2', requirements=['foo'])

            # Ambiguity purely within first-party deps.
            python_library(name="firstparty1", sources=["f1.py"])
            python_library(name="firstparty2", sources=["f1.py"])

            # Ambiguity within third-party, which should result in ambiguity for first-party too.
            # These all share the module `ambiguous.f2`.
            python_requirement_library(
                name='thirdparty3', requirements=['bar'], module_mapping={'bar': ['ambiguous.f2']}
            )
            python_requirement_library(
                name='thirdparty4', requirements=['bar'], module_mapping={'bar': ['ambiguous.f2']}
            )
            python_library(name="firstparty3", sources=["f2.py"])

            # Ambiguity within first-party, which should result in ambiguity for third-party too.
            # These all share the module `ambiguous.f3`.
            python_library(name="firstparty4", sources=["f3.py"])
            python_library(name="firstparty5", sources=["f3.py"])
            python_requirement_library(
                name='thirdparty5', requirements=['baz'], module_mapping={'baz': ['ambiguous.f3']}
            )
            """
        ),
    )
    assert_owners(
        "foo",
        expected=[],
        expected_ambiguous=[
            Address("source_root1/ambiguous", target_name="thirdparty1"),
            Address("source_root1/ambiguous", target_name="thirdparty2"),
        ],
    )
    assert_owners(
        "ambiguous.f1",
        expected=[],
        expected_ambiguous=[
            Address(
                "source_root1/ambiguous", relative_file_path="f1.py", target_name="firstparty1"
            ),
            Address(
                "source_root1/ambiguous", relative_file_path="f1.py", target_name="firstparty2"
            ),
        ],
    )
    assert_owners(
        "ambiguous.f2",
        expected=[],
        expected_ambiguous=[
            Address("source_root1/ambiguous", target_name="thirdparty3"),
            Address("source_root1/ambiguous", target_name="thirdparty4"),
            Address(
                "source_root1/ambiguous", relative_file_path="f2.py", target_name="firstparty3"
            ),
        ],
    )
    assert_owners(
        "ambiguous.f3",
        expected=[],
        expected_ambiguous=[
            Address("source_root1/ambiguous", target_name="thirdparty5"),
            Address(
                "source_root1/ambiguous", relative_file_path="f3.py", target_name="firstparty4"
            ),
            Address(
                "source_root1/ambiguous", relative_file_path="f3.py", target_name="firstparty5"
            ),
        ],
    )
Example #16
0
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. Even though the module is the same, we
    # special-case it to be legal for both file targets to be inferred.
    rule_runner.create_files("src/python/stubs", ["stub.py", "stub.pyi"])
    rule_runner.add_to_build_file("src/python/stubs", "python_library()")

    # 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')
            """
        ),
    )

    # If a module is ambiguous within a particular implementation, which means that it's not used
    # in that implementation's final mapping, it should still trigger ambiguity with another
    # implementation. Here, we have ambiguity with the Protobuf targets, but the Python file has
    # no ambiguity with other Python files; the Protobuf ambiguity needs to result in Python
    # being ambiguous.
    rule_runner.create_files("src/python/protos_ambiguous", ["f.proto", "f_pb2.py"])
    rule_runner.add_to_build_file(
        "src/python/protos_ambiguous",
        dedent(
            """\
            protobuf_library(name='protos1')
            protobuf_library(name='protos2')
            python_library(name='py')
            """
        ),
    )

    result = rule_runner.request(FirstPartyPythonModuleMapping, [])
    assert result == FirstPartyPythonModuleMapping(
        mapping=FrozenDict(
            {
                "project.util.dirutil": (
                    Address("src/python/project/util", relative_file_path="dirutil.py"),
                ),
                "project.util.tarutil": (
                    Address("src/python/project/util", relative_file_path="tarutil.py"),
                ),
                "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"),
                ),
            }
        ),
        ambiguous_modules=FrozenDict(
            {
                "protos.f2_pb2": (
                    Address(
                        "src/python/protos", relative_file_path="f2.proto", target_name="protos"
                    ),
                    Address("src/python/protos", relative_file_path="f2_pb2.py", target_name="py"),
                ),
                "protos_ambiguous.f_pb2": (
                    Address(
                        "src/python/protos_ambiguous",
                        relative_file_path="f.proto",
                        target_name="protos1",
                    ),
                    Address(
                        "src/python/protos_ambiguous",
                        relative_file_path="f.proto",
                        target_name="protos2",
                    ),
                    Address(
                        "src/python/protos_ambiguous",
                        relative_file_path="f_pb2.py",
                        target_name="py",
                    ),
                ),
                "two_owners": (
                    Address("build-support", relative_file_path="two_owners.py"),
                    Address("src/python", relative_file_path="two_owners.py"),
                ),
            }
        ),
    )