Example #1
0
async def inject_python_distribution_dependencies(
        request: InjectPythonDistributionDependencies,
        python_infer_subsystem: PythonInferSubsystem) -> InjectedDependencies:
    """Inject dependencies that we can infer from entry points in the distribution."""
    if not python_infer_subsystem.entry_points:
        return InjectedDependencies()

    original_tgt = await Get(WrappedTarget, Address,
                             request.dependencies_field.address)
    explicitly_provided_deps, distribution_entry_points, provides_entry_points = await MultiGet(
        Get(ExplicitlyProvidedDependencies,
            DependenciesRequest(original_tgt.target[Dependencies])),
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                entry_points_field=original_tgt.
                target[PythonDistributionEntryPointsField]),
        ),
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                provides_field=original_tgt.target[PythonProvidesField]),
        ),
    )

    address = original_tgt.target.address
    all_module_entry_points = [
        (category, name, entry_point) for category, entry_points in chain(
            distribution_entry_points.explicit_modules.items(),
            provides_entry_points.explicit_modules.items(),
        ) for name, entry_point in entry_points.items()
    ]
    all_module_owners = iter(await MultiGet(
        Get(PythonModuleOwners,
            PythonModuleOwnersRequest(entry_point.module, resolve=None))
        for _, _, entry_point in all_module_entry_points))
    module_owners: OrderedSet[Address] = OrderedSet()
    for (category, name, entry_point), owners in zip(all_module_entry_points,
                                                     all_module_owners):
        field_str = repr({category: {name: entry_point.spec}})
        explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
            owners.ambiguous,
            address,
            import_reference="module",
            context=(
                f"The python_distribution target {address} has the field "
                f"`entry_points={field_str}`, which maps to the Python module"
                f"`{entry_point.module}`"),
        )
        maybe_disambiguated = explicitly_provided_deps.disambiguated(
            owners.ambiguous)
        unambiguous_owners = owners.unambiguous or (
            (maybe_disambiguated, ) if maybe_disambiguated else ())
        module_owners.update(unambiguous_owners)

    return InjectedDependencies(
        Addresses(module_owners) +
        distribution_entry_points.pex_binary_addresses +
        provides_entry_points.pex_binary_addresses)
Example #2
0
def test_resolve_python_distribution_entry_points_required_fields() -> None:
    with pytest.raises(AssertionError):
        # either `entry_points_field` or `provides_field` is required
        ResolvePythonDistributionEntryPointsRequest()
Example #3
0
async def determine_finalized_setup_kwargs(request: GenerateSetupPyRequest) -> FinalizedSetupKwargs:
    exported_target = request.exported_target
    sources = request.sources
    requirements = await Get(ExportedTargetRequirements, DependencyOwner(exported_target))

    # Generate the kwargs for the setup() call. In addition to using the kwargs that are either
    # explicitly provided or generated via a user's plugin, we add additional kwargs based on the
    # resolved requirements and sources.
    target = exported_target.target
    resolved_setup_kwargs = await Get(SetupKwargs, ExportedTarget, exported_target)
    setup_kwargs = resolved_setup_kwargs.kwargs.copy()

    # NB: We are careful to not overwrite these values, but we also don't expect them to have been
    # set. The user must have have gone out of their way to use a `SetupKwargs` plugin, and to have
    # specified `SetupKwargs(_allow_banned_keys=True)`.
    setup_kwargs.update(
        {
            "packages": (*sources.packages, *(setup_kwargs.get("packages", []))),
            "namespace_packages": (
                *sources.namespace_packages,
                *setup_kwargs.get("namespace_packages", []),
            ),
            "package_data": {**dict(sources.package_data), **setup_kwargs.get("package_data", {})},
            "install_requires": (*requirements, *setup_kwargs.get("install_requires", [])),
        }
    )

    long_description_path = exported_target.target.get(LongDescriptionPathField).value

    if "long_description" in setup_kwargs and long_description_path:
        raise InvalidFieldException(
            f"The {repr(LongDescriptionPathField.alias)} field of the "
            f"target {exported_target.target.address} is set, but "
            f"'long_description' is already provided explicitly in "
            f"the provides=setup_py() field. You may only set one "
            f"of these two values."
        )

    if long_description_path:
        digest_contents = await Get(
            DigestContents,
            PathGlobs(
                [long_description_path],
                description_of_origin=(
                    f"the {LongDescriptionPathField.alias} "
                    f"field of {exported_target.target.address}"
                ),
                glob_match_error_behavior=GlobMatchErrorBehavior.error,
            ),
        )
        long_description_content = digest_contents[0].content.decode()
        setup_kwargs.update({"long_description": long_description_content})

    # Resolve entry points from python_distribution(entry_points=...) and from
    # python_distribution(provides=setup_py(entry_points=...)
    resolved_from_entry_points_field, resolved_from_provides_field = await MultiGet(
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                entry_points_field=exported_target.target.get(PythonDistributionEntryPointsField)
            ),
        ),
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                provides_field=exported_target.target.get(PythonProvidesField)
            ),
        ),
    )

    def _format_entry_points(
        resolved: ResolvedPythonDistributionEntryPoints,
    ) -> dict[str, dict[str, str]]:
        return {
            category: {ep_name: ep_val.entry_point.spec for ep_name, ep_val in entry_points.items()}
            for category, entry_points in resolved.val.items()
        }

    # Gather entry points with source description for any error messages when merging them.
    exported_addr = exported_target.target.address
    entry_point_sources = {
        f"{exported_addr}'s field `entry_points`": _format_entry_points(
            resolved_from_entry_points_field
        ),
        f"{exported_addr}'s field `provides=setup_py()`": _format_entry_points(
            resolved_from_provides_field
        ),
    }

    # Merge all collected entry points and add them to the dist's entry points.
    all_entry_points = merge_entry_points(*list(entry_point_sources.items()))
    if all_entry_points:
        setup_kwargs["entry_points"] = {
            category: [f"{name} = {entry_point}" for name, entry_point in entry_points.items()]
            for category, entry_points in all_entry_points.items()
        }

    return FinalizedSetupKwargs(setup_kwargs, address=target.address)
Example #4
0
async def determine_finalized_setup_kwargs(request: GenerateSetupPyRequest) -> FinalizedSetupKwargs:
    exported_target = request.exported_target
    sources = request.sources
    requirements = await Get(ExportedTargetRequirements, DependencyOwner(exported_target))

    # Generate the kwargs for the setup() call. In addition to using the kwargs that are either
    # explicitly provided or generated via a user's plugin, we add additional kwargs based on the
    # resolved requirements and sources.
    target = exported_target.target
    resolved_setup_kwargs = await Get(SetupKwargs, ExportedTarget, exported_target)
    setup_kwargs = resolved_setup_kwargs.kwargs.copy()

    # Check interpreter constraints
    if len(request.interpreter_constraints) > 1:
        raise SetupPyError(
            softwrap(
                f"""
                Expected a single interpreter constraint for {target.address}, got:
                {request.interpreter_constraints}.

                Python distributions do not support multiple constraints, so this will need to be
                translated into a single interpreter constraint using exclusions to get the same
                effect.

                As example, given two constraints:

                    >=2.7,<3 OR >=3.5,<3.11

                these can be combined into a single constraint using exclusions:

                    >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<3.11

                """
            )
        )
    if len(request.interpreter_constraints) > 0:
        # Do not replace value if already set.
        setup_kwargs.setdefault(
            "python_requires",
            # Pick the first constraint using a generator detour, as the InterpreterConstraints is
            # based on a FrozenOrderedSet which is not indexable.
            next(str(ic.specifier) for ic in request.interpreter_constraints),  # type: ignore[attr-defined]
        )

    # NB: We are careful to not overwrite these values, but we also don't expect them to have been
    # set. The user must have have gone out of their way to use a `SetupKwargs` plugin, and to have
    # specified `SetupKwargs(_allow_banned_keys=True)`.
    setup_kwargs.update(
        {
            "packages": (*sources.packages, *(setup_kwargs.get("packages", []))),
            "namespace_packages": (
                *sources.namespace_packages,
                *setup_kwargs.get("namespace_packages", []),
            ),
            "package_data": {**dict(sources.package_data), **setup_kwargs.get("package_data", {})},
            "install_requires": (*requirements, *setup_kwargs.get("install_requires", [])),
        }
    )

    long_description_path = exported_target.target.get(LongDescriptionPathField).value

    if "long_description" in setup_kwargs and long_description_path:
        raise InvalidFieldException(
            softwrap(
                f"""
                The {repr(LongDescriptionPathField.alias)} field of the
                target {exported_target.target.address} is set, but
                'long_description' is already provided explicitly in
                the provides=setup_py() field. You may only set one
                of these two values.
                """
            )
        )

    if long_description_path:
        digest_contents = await Get(
            DigestContents,
            PathGlobs(
                [long_description_path],
                description_of_origin=softwrap(
                    f"""
                    the {LongDescriptionPathField.alias}
                    field of {exported_target.target.address}
                    """
                ),
                glob_match_error_behavior=GlobMatchErrorBehavior.error,
            ),
        )
        long_description_content = digest_contents[0].content.decode()
        setup_kwargs.update({"long_description": long_description_content})

    # Resolve entry points from python_distribution(entry_points=...) and from
    # python_distribution(provides=setup_py(entry_points=...)
    resolved_from_entry_points_field, resolved_from_provides_field = await MultiGet(
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                entry_points_field=exported_target.target.get(PythonDistributionEntryPointsField)
            ),
        ),
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                provides_field=exported_target.target.get(PythonProvidesField)
            ),
        ),
    )

    def _format_entry_points(
        resolved: ResolvedPythonDistributionEntryPoints,
    ) -> dict[str, dict[str, str]]:
        return {
            category: {ep_name: ep_val.entry_point.spec for ep_name, ep_val in entry_points.items()}
            for category, entry_points in resolved.val.items()
        }

    # Gather entry points with source description for any error messages when merging them.
    exported_addr = exported_target.target.address
    entry_point_sources = {
        f"{exported_addr}'s field `entry_points`": _format_entry_points(
            resolved_from_entry_points_field
        ),
        f"{exported_addr}'s field `provides=setup_py()`": _format_entry_points(
            resolved_from_provides_field
        ),
    }

    # Merge all collected entry points and add them to the dist's entry points.
    all_entry_points = merge_entry_points(*list(entry_point_sources.items()))
    if all_entry_points:
        setup_kwargs["entry_points"] = {
            category: [f"{name} = {entry_point}" for name, entry_point in entry_points.items()]
            for category, entry_points in all_entry_points.items()
        }

    return FinalizedSetupKwargs(setup_kwargs, address=target.address)
Example #5
0
async def generate_chroot(request: SetupPyChrootRequest) -> SetupPyChroot:
    exported_target = request.exported_target
    exported_addr = exported_target.target.address

    owned_deps, transitive_targets = await MultiGet(
        Get(OwnedDependencies, DependencyOwner(exported_target)),
        Get(
            TransitiveTargets,
            TransitiveTargetsRequest([exported_target.target.address]),
        ),
    )
    # files() targets aren't owned by a single exported target - they aren't code, so
    # we allow them to be in multiple dists. This is helpful for, e.g., embedding
    # a standard license file in a dist.
    files_targets = (tgt for tgt in transitive_targets.closure
                     if tgt.has_field(FilesSources))
    targets = Targets(
        itertools.chain((od.target for od in owned_deps), files_targets))

    sources, requirements = await MultiGet(
        Get(SetupPySources, SetupPySourcesRequest(targets, py2=request.py2)),
        Get(ExportedTargetRequirements, DependencyOwner(exported_target)),
    )
    # Generate the kwargs for the setup() call. In addition to using the kwargs that are either
    # explicitly provided or generated via a user's plugin, we add additional kwargs based on the
    # resolved requirements and sources.
    target = exported_target.target
    resolved_setup_kwargs = await Get(SetupKwargs, ExportedTarget,
                                      exported_target)
    setup_kwargs = resolved_setup_kwargs.kwargs.copy()

    setup_script = setup_kwargs.pop("setup_script", None)
    if setup_script:
        # The target points to an existing setup.py script, so use that.
        invalid_keys = set(setup_kwargs.keys()) - {"name", "version"}
        if invalid_keys:
            raise InvalidSetupPyArgs(
                f"The `provides` field in {exported_addr} specifies a setup_script kwarg, so it "
                f"must only specify the name and version kwargs, but it also specified "
                f"{','.join(sorted(invalid_keys))}.")
        stripped_setup_script = await Get(
            StrippedSourceFileNames,
            SourcesPaths(files=(os.path.join(target.address.spec_path,
                                             setup_script), ),
                         dirs=()),
        )
        return SetupPyChroot(
            sources.digest,
            stripped_setup_script[0],
            FinalizedSetupKwargs(setup_kwargs, address=target.address),
        )

    # There is no existing setup.py script, so we generate one.

    # NB: We are careful to not overwrite these values, but we also don't expect them to have been
    # set. The user must have have gone out of their way to use a `SetupKwargs` plugin, and to have
    # specified `SetupKwargs(_allow_banned_keys=True)`.
    setup_kwargs.update({
        "packages": (*sources.packages, *(setup_kwargs.get("packages", []))),
        "namespace_packages": (
            *sources.namespace_packages,
            *setup_kwargs.get("namespace_packages", []),
        ),
        "package_data": {
            **dict(sources.package_data),
            **setup_kwargs.get("package_data", {})
        },
        "install_requires":
        (*requirements, *setup_kwargs.get("install_requires", [])),
    })

    # Resolve entry points from python_distribution(entry_points=...) and from
    # python_distribution(provides=setup_py(entry_points=...)
    resolved_from_entry_points_field, resolved_from_provides_field = await MultiGet(
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                entry_points_field=exported_target.target.get(
                    PythonDistributionEntryPointsField)),
        ),
        Get(
            ResolvedPythonDistributionEntryPoints,
            ResolvePythonDistributionEntryPointsRequest(
                provides_field=exported_target.target.get(
                    PythonProvidesField)),
        ),
    )

    def _format_entry_points(
        resolved: ResolvedPythonDistributionEntryPoints,
    ) -> Dict[str, Dict[str, str]]:
        return {
            category: {
                ep_name: ep_val.entry_point.spec
                for ep_name, ep_val in entry_points.items()
            }
            for category, entry_points in resolved.val.items()
        }

    # Gather entry points with source description for any error messages when merging them.
    entry_point_sources = {
        f"{exported_addr}'s field `entry_points`":
        _format_entry_points(resolved_from_entry_points_field),
        f"{exported_addr}'s field `provides=setup_py()`":
        _format_entry_points(resolved_from_provides_field),
    }

    # Merge all collected entry points and add them to the dist's entry points.
    all_entry_points = merge_entry_points(*list(entry_point_sources.items()))
    if all_entry_points:
        setup_kwargs["entry_points"] = {
            category: [
                f"{name} = {entry_point}"
                for name, entry_point in entry_points.items()
            ]
            for category, entry_points in all_entry_points.items()
        }

    # Generate the setup script.
    setup_py_content = SETUP_BOILERPLATE.format(
        target_address_spec=target.address.spec,
        setup_kwargs_str=distutils_repr(setup_kwargs),
    ).encode()
    files_to_create = [
        FileContent("setup.py", setup_py_content),
        FileContent("MANIFEST.in", "include *.py".encode()),
    ]
    extra_files_digest = await Get(Digest, CreateDigest(files_to_create))
    chroot_digest = await Get(
        Digest, MergeDigests((sources.digest, extra_files_digest)))
    return SetupPyChroot(
        chroot_digest, "setup.py",
        FinalizedSetupKwargs(setup_kwargs, address=target.address))