New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deprecate python_requirements
and poetry_requirements
using old macro in favor of target generation
#14075
Merged
Eric-Arellano
merged 12 commits into
pantsbuild:main
from
Eric-Arellano:python-reqs-generators
Jan 14, 2022
Merged
Deprecate python_requirements
and poetry_requirements
using old macro in favor of target generation
#14075
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
c368b09
Add `--use-deprecated-python-macros`
Eric-Arellano 87a7092
Add pipenv_requirements.py
Eric-Arellano 3c8add9
Add `python_requirements` target generator
Eric-Arellano 1272709
Move Poetry helpers into poetry_requirements.py
Eric-Arellano d80d8f3
Add poetry_requirements
Eric-Arellano 204f8c5
Add the deprecation and `--update-build-files-python-macros`
Eric-Arellano 235f591
Switch Pants to target generators
Eric-Arellano 9904503
Register the rules, oops!
Eric-Arellano 2b4dcad
Stu review feedback about help messages
Eric-Arellano 0d48329
Fix tests
Eric-Arellano 33eb52a
Merge branch 'main' of github.com:pantsbuild/pants into python-reqs-g…
Eric-Arellano ad08c88
Bump timeout for Scala Protobuf test
Eric-Arellano File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Any, ClassVar, Dict, Iterable, Tuple | ||
|
||
from packaging.utils import canonicalize_name as canonicalize_project_name | ||
|
||
from pants.backend.python.target_types import ( | ||
PythonRequirementModulesField, | ||
PythonRequirementTarget, | ||
PythonRequirementTypeStubModulesField, | ||
normalize_module_mapping, | ||
) | ||
from pants.engine.addresses import Address | ||
from pants.engine.target import DictStringToStringSequenceField, OverridesField | ||
from pants.util.frozendict import FrozenDict | ||
|
||
|
||
class ModuleMappingField(DictStringToStringSequenceField): | ||
alias = "module_mapping" | ||
help = ( | ||
"A mapping of requirement names to a list of the modules they provide.\n\n" | ||
'For example, `{"ansicolors": ["colors"]}`.\n\n' | ||
"Any unspecified requirements will use a default. See the " | ||
f"`{PythonRequirementModulesField.alias}` field from the `{PythonRequirementTarget.alias}` " | ||
f"target for more information." | ||
) | ||
value: FrozenDict[str, tuple[str, ...]] | ||
default: ClassVar[FrozenDict[str, tuple[str, ...]]] = FrozenDict() | ||
|
||
@classmethod | ||
def compute_value( # type: ignore[override] | ||
cls, raw_value: Dict[str, Iterable[str]], address: Address | ||
) -> FrozenDict[str, Tuple[str, ...]]: | ||
value_or_default = super().compute_value(raw_value, address) | ||
return normalize_module_mapping(value_or_default) | ||
|
||
|
||
class TypeStubsModuleMappingField(DictStringToStringSequenceField): | ||
alias = "type_stubs_module_mapping" | ||
help = ( | ||
Eric-Arellano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"A mapping of type-stub requirement names to a list of the modules they provide.\n\n" | ||
'For example, `{"types-requests": ["requests"]}`.\n\n' | ||
"If the requirement is not specified _and_ its name looks like a type stub, Pants will " | ||
f"use a default. See the `{PythonRequirementTypeStubModulesField.alias}` field from the " | ||
f"`{PythonRequirementTarget.alias}` target for more information." | ||
) | ||
value: FrozenDict[str, tuple[str, ...]] | ||
default: ClassVar[FrozenDict[str, tuple[str, ...]]] = FrozenDict() | ||
|
||
@classmethod | ||
def compute_value( # type: ignore[override] | ||
cls, raw_value: Dict[str, Iterable[str]], address: Address | ||
) -> FrozenDict[str, Tuple[str, ...]]: | ||
value_or_default = super().compute_value(raw_value, address) | ||
return normalize_module_mapping(value_or_default) | ||
|
||
|
||
class RequirementsOverrideField(OverridesField): | ||
help = ( | ||
"Override the field values for generated `python_requirement` targets.\n\n" | ||
"Expects a dictionary of requirements to a dictionary for the " | ||
"overrides. You may either use a string for a single requirement, " | ||
"or a string tuple for multiple requirements. Each override is a dictionary of " | ||
"field names to the overridden value.\n\n" | ||
"For example:\n\n" | ||
"```\n" | ||
"overrides={\n" | ||
' "django": {"dependencies": ["#setuptools"]]},\n' | ||
' "ansicolors": {"description": "pretty colors"]},\n' | ||
' ("ansicolors, "django"): {"tags": ["overridden"]},\n' | ||
"}\n" | ||
"```\n\n" | ||
"Every overridden requirement is validated to be generated by this target.\n\n" | ||
"You can specify the same requirement in multiple keys, so long as you don't " | ||
"override the same field more than one time for the requirement." | ||
) | ||
|
||
def flatten_and_normalize(self) -> dict[str, dict[str, Any]]: | ||
return {canonicalize_project_name(req): v for req, v in super().flatten().items()} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
src/python/pants/backend/python/macros/pipenv_requirements.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import annotations | ||
|
||
import json | ||
|
||
from packaging.utils import canonicalize_name as canonicalize_project_name | ||
|
||
from pants.backend.python.macros.common_fields import ( | ||
ModuleMappingField, | ||
RequirementsOverrideField, | ||
TypeStubsModuleMappingField, | ||
) | ||
from pants.backend.python.pip_requirement import PipRequirement | ||
from pants.backend.python.target_types import ( | ||
PythonRequirementModulesField, | ||
PythonRequirementsField, | ||
PythonRequirementsFileSourcesField, | ||
PythonRequirementsFileTarget, | ||
PythonRequirementTarget, | ||
PythonRequirementTypeStubModulesField, | ||
) | ||
from pants.engine.addresses import Address | ||
from pants.engine.fs import DigestContents, GlobMatchErrorBehavior, PathGlobs | ||
from pants.engine.rules import Get, collect_rules, rule | ||
from pants.engine.target import ( | ||
COMMON_TARGET_FIELDS, | ||
Dependencies, | ||
GeneratedTargets, | ||
GenerateTargetsRequest, | ||
InvalidFieldException, | ||
SingleSourceField, | ||
StringField, | ||
Target, | ||
) | ||
from pants.engine.unions import UnionRule | ||
from pants.util.logging import LogLevel | ||
|
||
|
||
class PipenvSourceField(SingleSourceField): | ||
default = "Pipfile.lock" | ||
required = False | ||
|
||
|
||
class PipenvPipfileTargetField(StringField): | ||
alias = "pipfile_target" | ||
help = "Deprecated: no longer necessary." | ||
removal_version = "2.11.0.dev0" | ||
removal_hint = "This field is no longer necessary." | ||
|
||
|
||
class PipenvRequirementsTargetGenerator(Target): | ||
alias = "pipenv_requirements" | ||
help = "Generate a `python_requirement` for each entry in `Pipenv.lock`." | ||
core_fields = ( | ||
*COMMON_TARGET_FIELDS, | ||
ModuleMappingField, | ||
TypeStubsModuleMappingField, | ||
PipenvSourceField, | ||
PipenvPipfileTargetField, | ||
RequirementsOverrideField, | ||
) | ||
|
||
|
||
class GenerateFromPipenvRequirementsRequest(GenerateTargetsRequest): | ||
generate_from = PipenvRequirementsTargetGenerator | ||
|
||
|
||
# TODO(#10655): add support for PEP 440 direct references (aka VCS style). | ||
# TODO(#10655): differentiate between Pipfile vs. Pipfile.lock. | ||
@rule(desc="Generate `python_requirement` targets from Pipfile.lock", level=LogLevel.DEBUG) | ||
async def generate_from_pipenv_requirement( | ||
request: GenerateFromPipenvRequirementsRequest, | ||
) -> GeneratedTargets: | ||
generator = request.generator | ||
lock_rel_path = generator[PipenvSourceField].value | ||
lock_full_path = generator[PipenvSourceField].file_path | ||
|
||
file_tgt = PythonRequirementsFileTarget( | ||
{PythonRequirementsFileSourcesField.alias: lock_rel_path}, | ||
Address( | ||
generator.address.spec_path, | ||
target_name=generator.address.target_name, | ||
relative_file_path=lock_rel_path, | ||
), | ||
) | ||
|
||
digest_contents = await Get( | ||
DigestContents, | ||
PathGlobs( | ||
[lock_full_path], | ||
glob_match_error_behavior=GlobMatchErrorBehavior.error, | ||
description_of_origin=f"{generator}'s field `{PipenvSourceField.alias}`", | ||
), | ||
) | ||
lock_info = json.loads(digest_contents[0].content) | ||
|
||
module_mapping = generator[ModuleMappingField].value | ||
stubs_mapping = generator[TypeStubsModuleMappingField].value | ||
overrides = generator[RequirementsOverrideField].flatten_and_normalize() | ||
|
||
def generate_tgt(raw_req: str, info: dict) -> PythonRequirementTarget: | ||
if info.get("extras"): | ||
raw_req += f"[{','.join(info['extras'])}]" | ||
raw_req += info.get("version", "") | ||
if info.get("markers"): | ||
raw_req += f";{info['markers']}" | ||
|
||
parsed_req = PipRequirement.parse(raw_req) | ||
normalized_proj_name = canonicalize_project_name(parsed_req.project_name) | ||
tgt_overrides = overrides.pop(normalized_proj_name, {}) | ||
if Dependencies.alias in tgt_overrides: | ||
tgt_overrides[Dependencies.alias] = list(tgt_overrides[Dependencies.alias]) + [ | ||
file_tgt.address.spec | ||
] | ||
|
||
# TODO: Consider letting you set metadata in the target generator and having it pass down | ||
# to all generated targets. Especially useful for compatible_resolves. | ||
return PythonRequirementTarget( | ||
{ | ||
PythonRequirementsField.alias: [parsed_req], | ||
PythonRequirementModulesField.alias: module_mapping.get(normalized_proj_name), | ||
PythonRequirementTypeStubModulesField.alias: stubs_mapping.get( | ||
normalized_proj_name | ||
), | ||
# This may get overridden by `tgt_overrides`, which will have already added in | ||
# the file tgt. | ||
Dependencies.alias: [file_tgt.address.spec], | ||
**tgt_overrides, | ||
}, | ||
generator.address.create_generated(parsed_req.project_name), | ||
) | ||
|
||
result = tuple( | ||
generate_tgt(req, info) | ||
for req, info in {**lock_info.get("default", {}), **lock_info.get("develop", {})}.items() | ||
) + (file_tgt,) | ||
|
||
if overrides: | ||
raise InvalidFieldException( | ||
f"Unused key in the `overrides` field for {request.generator.address}: " | ||
f"{sorted(overrides)}" | ||
) | ||
|
||
return GeneratedTargets(generator, result) | ||
|
||
|
||
def rules(): | ||
return ( | ||
*collect_rules(), | ||
UnionRule(GenerateTargetsRequest, GenerateFromPipenvRequirementsRequest), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still weird to me (seems like a double-negative), but oh well!