Skip to content

Commit

Permalink
Add Python tool support for console scripts.
Browse files Browse the repository at this point in the history
Introduce `MainSpecification` which can either be an `EntryPoint` or
a `ConsoleScript`. Both `PexRequest` and `PexFromTargetsRequest` are
updated to take a `MainSpecification` as is `PythonToolBase`. All
internal rules are updated to use `ConsoleScript` where possible for
improved stability of entry points across requirement upgrades.

Fixes #11583

[ci skip-rust]
[ci skip-build-wheels]
  • Loading branch information
jsirois committed Mar 2, 2021
1 parent 87f2261 commit 2e913f4
Show file tree
Hide file tree
Showing 30 changed files with 209 additions and 99 deletions.
3 changes: 2 additions & 1 deletion src/python/pants/backend/awslambda/python/lambdex.py
Expand Up @@ -2,6 +2,7 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript


class Lambdex(PythonToolBase):
Expand All @@ -14,4 +15,4 @@ class Lambdex(PythonToolBase):
default_extra_requirements = ["setuptools>=50.3.0,<50.4"]
register_interpreter_constraints = True
default_interpreter_constraints = ["CPython>=3.5"]
default_entry_point = "lambdex.bin.lambdex"
default_main = ConsoleScript("lambdex")
4 changes: 2 additions & 2 deletions src/python/pants/backend/awslambda/python/rules.py
Expand Up @@ -69,7 +69,7 @@ async def package_python_awslambda(
PexFromTargetsRequest(
addresses=[field_set.address],
internal_only=False,
entry_point=None,
main=None,
output_filename=output_filename,
platforms=PexPlatforms([platform]),
additional_args=[
Expand All @@ -87,7 +87,7 @@ async def package_python_awslambda(
internal_only=True,
requirements=PexRequirements(lambdex.all_requirements),
interpreter_constraints=PexInterpreterConstraints(lambdex.interpreter_constraints),
entry_point=lambdex.entry_point,
main=lambdex.main,
)

lambdex_pex, pex_result, handler = await MultiGet(
Expand Down
5 changes: 3 additions & 2 deletions src/python/pants/backend/python/goals/coverage_py.py
Expand Up @@ -11,6 +11,7 @@
from typing import List, Optional, Tuple, cast

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript
from pants.backend.python.util_rules.pex import (
PexInterpreterConstraints,
PexRequest,
Expand Down Expand Up @@ -99,7 +100,7 @@ class CoverageSubsystem(PythonToolBase):
help = "Configuration for Python test coverage measurement."

default_version = "coverage>=5.0.3,<5.1"
default_entry_point = "coverage"
default_main = ConsoleScript("coverage")
register_interpreter_constraints = True
default_interpreter_constraints = ["CPython>=3.6"]

Expand Down Expand Up @@ -228,7 +229,7 @@ async def setup_coverage(coverage: CoverageSubsystem) -> CoverageSetup:
internal_only=True,
requirements=PexRequirements(coverage.all_requirements),
interpreter_constraints=PexInterpreterConstraints(coverage.interpreter_constraints),
entry_point=coverage.entry_point,
main=coverage.main,
),
)
return CoverageSetup(pex)
Expand Down
4 changes: 3 additions & 1 deletion src/python/pants/backend/python/goals/package_pex_binary.py
Expand Up @@ -113,7 +113,9 @@ async def package_pex_binary(
PexFromTargetsRequest(
addresses=[field_set.address],
internal_only=False,
entry_point=resolved_entry_point.val,
# TODO(John Sirois): Support ConsoleScript in PexBinary targets:
# https://github.com/pantsbuild/pants/issues/11619
main=resolved_entry_point.val,
platforms=PexPlatforms.create_from_platforms_field(field_set.platforms),
output_filename=output_filename,
additional_args=field_set.generate_additional_args(pex_binary_defaults),
Expand Down
5 changes: 4 additions & 1 deletion src/python/pants/backend/python/goals/pytest_runner.py
Expand Up @@ -14,6 +14,7 @@
)
from pants.backend.python.subsystems.pytest import PyTest
from pants.backend.python.target_types import (
EntryPoint,
PythonRuntimePackageDependencies,
PythonTestsSources,
PythonTestsTimeout,
Expand Down Expand Up @@ -172,7 +173,9 @@ async def setup_pytest_for_target(
PexRequest(
output_filename="pytest_runner.pex",
interpreter_constraints=interpreter_constraints,
entry_point="pytest",
# TODO(John Sirois): Switch to ConsoleScript once Pex supports discovering console
# scripts via the PEX_PATH: https://github.com/pantsbuild/pex/issues/1257
main=EntryPoint("pytest"),
internal_only=True,
pex_path=[pytest_pex, requirements_pex],
),
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/goals/repl.py
Expand Up @@ -78,7 +78,7 @@ async def create_ipython_repl_request(
Pex,
PexRequest(
output_filename="ipython.pex",
entry_point=ipython.entry_point,
main=ipython.main,
requirements=PexRequirements(ipython.all_requirements),
interpreter_constraints=requirements_pex_request.interpreter_constraints,
internal_only=True,
Expand Down
4 changes: 3 additions & 1 deletion src/python/pants/backend/python/goals/run_pex_binary.py
Expand Up @@ -62,7 +62,9 @@ async def create_pex_binary_run_request(
internal_only=True,
# Note that the entry point file is not in the PEX itself. It's loaded by setting
# `PEX_EXTRA_SYS_PATH`.
entry_point=entry_point.val,
# TODO(John Sirois): Support ConsoleScript in PexBinary targets:
# https://github.com/pantsbuild/pants/issues/11619
main=entry_point.val,
),
)

Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/goals/setup_py.py
Expand Up @@ -535,7 +535,7 @@ async def generate_chroot(request: SetupPyChrootRequest) -> SetupPyChroot:
f"{binary.address} left the field off. Set `entry_point` to either "
f"`app.py:func` or the longhand `path.to.app:func`. See {url}."
)
if ":" not in entry_point:
if not entry_point.function:
raise InvalidEntryPoint(
"Every `pex_binary` used in `with_binaries()` for the `provides()` field for "
f"{exported_addr} must end in the format `:my_func` for the `entry_point` field, "
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/lint/bandit/rules.py
Expand Up @@ -69,7 +69,7 @@ async def bandit_lint_partition(
internal_only=True,
requirements=PexRequirements(bandit.all_requirements),
interpreter_constraints=partition.interpreter_constraints,
entry_point=bandit.entry_point,
main=bandit.main,
),
)

Expand Down
3 changes: 2 additions & 1 deletion src/python/pants/backend/python/lint/bandit/subsystem.py
Expand Up @@ -4,6 +4,7 @@
from typing import Optional, Tuple, cast

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript
from pants.option.custom_types import file_option, shell_str


Expand All @@ -14,7 +15,7 @@ class Bandit(PythonToolBase):
default_version = "bandit>=1.6.2,<1.7"
# `setuptools<45` is for Python 2 support. `stevedore` is because the 3.0 release breaks Bandit.
default_extra_requirements = ["setuptools<45", "stevedore<3"]
default_entry_point = "bandit"
default_main = ConsoleScript("bandit")

@classmethod
def register_options(cls, register):
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/lint/black/rules.py
Expand Up @@ -101,7 +101,7 @@ async def setup_black(
internal_only=True,
requirements=PexRequirements(black.all_requirements),
interpreter_constraints=tool_interpreter_constraints,
entry_point=black.entry_point,
main=black.main,
),
)

Expand Down
3 changes: 2 additions & 1 deletion src/python/pants/backend/python/lint/black/subsystem.py
Expand Up @@ -4,6 +4,7 @@
from typing import Optional, Tuple, cast

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript
from pants.option.custom_types import file_option, shell_str


Expand All @@ -14,7 +15,7 @@ class Black(PythonToolBase):
# TODO: simplify `test_works_with_python39()` to stop using a VCS version.
default_version = "black==20.8b1"
default_extra_requirements = ["setuptools"]
default_entry_point = "black:patched_main"
default_main = ConsoleScript("black")
register_interpreter_constraints = True
default_interpreter_constraints = ["CPython>=3.6"]

Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/lint/docformatter/rules.py
Expand Up @@ -66,7 +66,7 @@ async def setup_docformatter(setup_request: SetupRequest, docformatter: Docforma
internal_only=True,
requirements=PexRequirements(docformatter.all_requirements),
interpreter_constraints=PexInterpreterConstraints(docformatter.interpreter_constraints),
entry_point=docformatter.entry_point,
main=docformatter.main,
),
)

Expand Down
Expand Up @@ -4,6 +4,7 @@
from typing import Tuple, cast

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript
from pants.option.custom_types import shell_str


Expand All @@ -12,7 +13,7 @@ class Docformatter(PythonToolBase):
help = "The Python docformatter tool (https://github.com/myint/docformatter)."

default_version = "docformatter>=1.3.1,<1.4"
default_entry_point = "docformatter"
default_main = ConsoleScript("docformatter")
register_interpreter_constraints = True
default_interpreter_constraints = ["CPython==2.7.*", "CPython>=3.4,<3.9"]

Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/lint/flake8/rules.py
Expand Up @@ -69,7 +69,7 @@ async def flake8_lint_partition(
internal_only=True,
requirements=PexRequirements(flake8.all_requirements),
interpreter_constraints=partition.interpreter_constraints,
entry_point=flake8.entry_point,
main=flake8.main,
),
)

Expand Down
3 changes: 2 additions & 1 deletion src/python/pants/backend/python/lint/flake8/subsystem.py
Expand Up @@ -4,6 +4,7 @@
from typing import Optional, Tuple, cast

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript
from pants.option.custom_types import file_option, shell_str


Expand All @@ -16,7 +17,7 @@ class Flake8(PythonToolBase):
"setuptools<45; python_full_version == '2.7.*'",
"setuptools; python_version > '2.7'",
]
default_entry_point = "flake8"
default_main = ConsoleScript("flake8")

@classmethod
def register_options(cls, register):
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/lint/isort/rules.py
Expand Up @@ -77,7 +77,7 @@ async def setup_isort(setup_request: SetupRequest, isort: Isort) -> Setup:
internal_only=True,
requirements=PexRequirements(isort.all_requirements),
interpreter_constraints=PexInterpreterConstraints(isort.interpreter_constraints),
entry_point=isort.entry_point,
main=isort.main,
),
)

Expand Down
3 changes: 2 additions & 1 deletion src/python/pants/backend/python/lint/isort/subsystem.py
Expand Up @@ -4,6 +4,7 @@
from typing import Tuple, cast

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript
from pants.option.custom_types import file_option, shell_str


Expand All @@ -15,7 +16,7 @@ class Isort(PythonToolBase):
default_extra_requirements = ["setuptools"]
register_interpreter_constraints = True
default_interpreter_constraints = ["CPython>=3.6"]
default_entry_point = "isort.main"
default_main = ConsoleScript("isort")

@classmethod
def register_options(cls, register):
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/backend/python/lint/pylint/rules.py
Expand Up @@ -179,7 +179,7 @@ async def pylint_lint_partition(partition: PylintPartition, pylint: Pylint) -> L
PexRequest(
output_filename="pylint_runner.pex",
interpreter_constraints=partition.interpreter_constraints,
entry_point=pylint.entry_point,
main=pylint.main,
internal_only=True,
pex_path=[pylint_pex, requirements_pex],
),
Expand Down
5 changes: 4 additions & 1 deletion src/python/pants/backend/python/lint/pylint/subsystem.py
Expand Up @@ -6,6 +6,7 @@
from typing import List, cast

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import EntryPoint
from pants.engine.addresses import UnparsedAddressInputs
from pants.option.custom_types import file_option, shell_str, target_option
from pants.util.docutil import docs_url
Expand All @@ -16,7 +17,9 @@ class Pylint(PythonToolBase):
help = "The Pylint linter for Python code (https://www.pylint.org/)."

default_version = "pylint>=2.4.4,<2.5"
default_entry_point = "pylint"
# TODO(John Sirois): Switch to ConsoleScript once Pex supports discovering console
# scripts via the PEX_PATH: https://github.com/pantsbuild/pex/issues/1257
default_main = EntryPoint("pylint")

@classmethod
def register_options(cls, register):
Expand Down
3 changes: 2 additions & 1 deletion src/python/pants/backend/python/subsystems/ipython.py
Expand Up @@ -2,14 +2,15 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from pants.backend.python.subsystems.python_tool_base import PythonToolBase
from pants.backend.python.target_types import ConsoleScript


class IPython(PythonToolBase):
options_scope = "ipython"
help = "The IPython enhanced REPL (https://ipython.org/)."

default_version = "ipython==7.16.1" # The last version to support Python 3.6.
default_entry_point = "IPython:start_ipython"
default_main = ConsoleScript("ipython")

@classmethod
def register_options(cls, register):
Expand Down
41 changes: 34 additions & 7 deletions src/python/pants/backend/python/subsystems/python_tool_base.py
@@ -1,8 +1,10 @@
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from typing import ClassVar, Optional, Sequence, Tuple, cast
from typing import ClassVar, Sequence, Tuple, cast

from pants.backend.python.target_types import ConsoleScript, EntryPoint, MainSpecification
from pants.option.errors import OptionsError
from pants.option.subsystem import Subsystem


Expand Down Expand Up @@ -52,22 +54,35 @@ class PythonToolBase(PythonToolRequirementsBase):
"""Base class for subsystems that configure a python tool to be invoked out-of-process."""

# Subclasses must set.
default_entry_point: ClassVar[str]
default_main: ClassVar[MainSpecification]
# Subclasses do not need to override.
default_interpreter_constraints: ClassVar[Sequence[str]] = []
register_interpreter_constraints: ClassVar[bool] = False

@classmethod
def register_options(cls, register):
super().register_options(register)
register(
"--console-script",
type=str,
advanced=True,
default=cls.default_main if isinstance(cls.default_main, ConsoleScript) else None,
help=(
"The console script for the tool. Using this option is generally preferable to "
"(and mutually exclusive with) specifying an --entry-point since console script "
"names have a higher expectation of staying stable across releases of the tool. "
"Usually, you will not want to change this from the default."
),
)
register(
"--entry-point",
type=str,
advanced=True,
default=cls.default_entry_point,
default=cls.default_main if isinstance(cls.default_main, EntryPoint) else None,
help=(
"The main module for the tool. Usually, you will not want to change this from the "
"default."
"The entry point for the tool. Generally you only want to use this option if the "
"tool does not offer a --console-script (which this option is mutually exclusive "
"with). Usually, you will not want to change this from the default."
),
)

Expand All @@ -92,5 +107,17 @@ def interpreter_constraints(self) -> Tuple[str, ...]:
return tuple(self.options.interpreter_constraints)

@property
def entry_point(self) -> Optional[str]:
return cast(Optional[str], self.options.entry_point)
def main(self) -> MainSpecification:
is_default_console_script = self.options.is_default("console_script")
is_default_entry_point = self.options.is_default("entry_point")
if not is_default_console_script and not is_default_entry_point:
raise OptionsError(
f"Both --console-script={self.options.console_script} and "
f"--entry-point={self.options.entry_point} are configured but these options are "
f"mutually exclusive. Pick one."
)
if not is_default_console_script:
return ConsoleScript(cast(str, self.options.console_script))
if not is_default_entry_point:
return EntryPoint.parse(cast(str, self.options.entry_point))
return self.default_main

0 comments on commit 2e913f4

Please sign in to comment.