diff --git a/src/python/pants/backend/python/goals/coverage_py.py b/src/python/pants/backend/python/goals/coverage_py.py index ab68c86b68f..5469c79fc9b 100644 --- a/src/python/pants/backend/python/goals/coverage_py.py +++ b/src/python/pants/backend/python/goals/coverage_py.py @@ -47,8 +47,15 @@ from pants.engine.rules import Get, MultiGet, collect_rules, rule from pants.engine.target import TransitiveTargets, TransitiveTargetsRequest from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option from pants.option.global_options import ProcessCleanupOption +from pants.option.option_types import ( + BoolOption, + EnumListOption, + FileOption, + FloatOption, + StrListOption, + StrOption, +) from pants.source.source_root import AllSourceRoots from pants.util.docutil import git_url from pants.util.logging import LogLevel @@ -113,104 +120,79 @@ class CoverageSubsystem(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/subsystems/coverage_py_lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--filter", - type=list, - member_type=str, - default=None, - help=( - "A list of Python modules or filesystem paths to use in the coverage report, e.g. " - "`['helloworld_test', 'helloworld/util/dirutil'].\n\nBoth modules and directory " - "paths are recursive: any submodules or child paths, respectively, will be " - "included.\n\nIf you leave this off, the coverage report will include every file " - "in the transitive closure of the address/file arguments; for example, `test ::` " - "will include every Python file in your project, whereas " - "`test project/app_test.py` will include `app_test.py` and any of its transitive " - "dependencies." - ), - ) - register( - "--report", - type=list, - member_type=CoverageReportType, - default=[CoverageReportType.CONSOLE], - help="Which coverage report type(s) to emit.", - ) - register( - "--output-dir", - type=str, - default=str(PurePath("dist", "coverage", "python")), - advanced=True, - help="Path to write the Pytest Coverage report to. Must be relative to build root.", - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to an INI or TOML config file understood by coverage.py " - "(https://coverage.readthedocs.io/en/stable/config.html).\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - f"this option if the config is located in a non-standard location." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include any relevant config files during runs " - "(`.coveragerc`, `setup.cfg`, `tox.ini`, and `pyproject.toml`)." - f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " - f"non-standard location." - ), - ) - register( - "--global-report", - type=bool, - default=False, - help=( - "If true, Pants will generate a global coverage report.\n\nThe global report will " - "include all Python source files in the workspace and not just those depended on " - "by the tests that were run." - ), - ) - register( - "--fail-under", - type=float, - default=None, - help=( - "Fail if the total combined coverage percentage for all tests is less than this " - "number.\n\nUse this instead of setting fail_under in a coverage.py config file, " - "as the config will apply to each test separately, while you typically want this " - "to apply to the combined coverage for all tests run." - "\n\nNote that you must generate at least one (non-raw) coverage report for this " - "check to trigger.\n\nNote also that if you specify a non-integral value, you must " - "also set [report] precision properly in the coverage.py config file to make use " - "of the decimal places. See https://coverage.readthedocs.io/en/latest/config.html ." - ), - ) - - @property - def filter(self) -> tuple[str, ...]: - return tuple(self.options.filter) - - @property - def reports(self) -> tuple[CoverageReportType, ...]: - return tuple(self.options.report) + filter = StrListOption( + "--filter", + help=( + "A list of Python modules or filesystem paths to use in the coverage report, e.g. " + "`['helloworld_test', 'helloworld/util/dirutil'].\n\nBoth modules and directory " + "paths are recursive: any submodules or child paths, respectively, will be " + "included.\n\nIf you leave this off, the coverage report will include every file " + "in the transitive closure of the address/file arguments; for example, `test ::` " + "will include every Python file in your project, whereas " + "`test project/app_test.py` will include `app_test.py` and any of its transitive " + "dependencies." + ), + ) + reports = EnumListOption( + "--report", + default=[CoverageReportType.CONSOLE], + help="Which coverage report type(s) to emit.", + ) + _output_dir = StrOption( + "--output-dir", + default=str(PurePath("dist", "coverage", "python")), + advanced=True, + help="Path to write the Pytest Coverage report to. Must be relative to build root.", + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=lambda cls: ( + "Path to an INI or TOML config file understood by coverage.py " + "(https://coverage.readthedocs.io/en/stable/config.html).\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + f"this option if the config is located in a non-standard location." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include any relevant config files during runs " + "(`.coveragerc`, `setup.cfg`, `tox.ini`, and `pyproject.toml`)." + f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " + f"non-standard location." + ), + ) + global_report = BoolOption( + "--global-report", + default=False, + help=( + "If true, Pants will generate a global coverage report.\n\nThe global report will " + "include all Python source files in the workspace and not just those depended on " + "by the tests that were run." + ), + ) + fail_under = FloatOption( + "--fail-under", + default=None, + help=( + "Fail if the total combined coverage percentage for all tests is less than this " + "number.\n\nUse this instead of setting fail_under in a coverage.py config file, " + "as the config will apply to each test separately, while you typically want this " + "to apply to the combined coverage for all tests run." + "\n\nNote that you must generate at least one (non-raw) coverage report for this " + "check to trigger.\n\nNote also that if you specify a non-integral value, you must " + "also set [report] precision properly in the coverage.py config file to make use " + "of the decimal places. See https://coverage.readthedocs.io/en/latest/config.html ." + ), + ) @property def output_dir(self) -> PurePath: - return PurePath(self.options.output_dir) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + return PurePath(self._output_dir) @property def config_request(self) -> ConfigFilesRequest: @@ -218,7 +200,7 @@ def config_request(self) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"[{self.options_scope}].config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_existence=[".coveragerc"], check_content={ "setup.cfg": b"[coverage:", @@ -227,14 +209,6 @@ def config_request(self) -> ConfigFilesRequest: }, ) - @property - def global_report(self) -> bool: - return cast(bool, self.options.global_report) - - @property - def fail_under(self) -> int: - return cast(int, self.options.fail_under) - class CoveragePyLockfileSentinel(GenerateToolLockfileSentinel): resolve_name = CoverageSubsystem.options_scope diff --git a/src/python/pants/backend/python/lint/autoflake/subsystem.py b/src/python/pants/backend/python/lint/autoflake/subsystem.py index 3bd54689054..0327bc80a8b 100644 --- a/src/python/pants/backend/python/lint/autoflake/subsystem.py +++ b/src/python/pants/backend/python/lint/autoflake/subsystem.py @@ -3,8 +3,6 @@ from __future__ import annotations -from typing import cast - from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile from pants.backend.python.subsystems.python_tool_base import PythonToolBase @@ -12,7 +10,7 @@ from pants.core.goals.generate_lockfiles import GenerateToolLockfileSentinel from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule -from pants.option.custom_types import shell_str +from pants.option.option_types import ArgsListOption, BoolOption from pants.util.docutil import bin_name, git_url @@ -31,32 +29,17 @@ class Autoflake(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/lint/autoflake/lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use Autoflake when running `{bin_name()} fmt` and `{bin_name()} lint`", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to Autoflake, e.g. " - f'`--{cls.options_scope}-args="--target-version=py37 --quiet"`' - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use Autoflake when running `{bin_name()} fmt` and `{bin_name()} lint`", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to Autoflake, e.g. " + f'`--{cls.options_scope}-args="--target-version=py37 --quiet"`' + ), + ) class AutoflakeLockfileSentinel(GenerateToolLockfileSentinel): diff --git a/src/python/pants/backend/python/lint/bandit/subsystem.py b/src/python/pants/backend/python/lint/bandit/subsystem.py index 989f0aacebe..8c380f333d7 100644 --- a/src/python/pants/backend/python/lint/bandit/subsystem.py +++ b/src/python/pants/backend/python/lint/bandit/subsystem.py @@ -5,7 +5,6 @@ import itertools from dataclasses import dataclass -from typing import cast from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -23,7 +22,7 @@ from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import AllTargets, AllTargetsRequest, FieldSet, Target from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str +from pants.option.option_types import ArgsListOption, BoolOption, FileOption from pants.util.docutil import bin_name, git_url from pants.util.logging import LogLevel @@ -61,46 +60,26 @@ class Bandit(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/lint/bandit/lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use Bandit when running `{bin_name()} lint`", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - f"Arguments to pass directly to Bandit, e.g. " - f'`--{cls.options_scope}-args="--skip B101,B308 --confidence"`' - ), - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to a Bandit YAML config file " - "(https://bandit.readthedocs.io/en/latest/config.html)." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use Bandit when running `{bin_name()} lint`", + ) + args = ArgsListOption( + help=lambda cls: ( + f"Arguments to pass directly to Bandit, e.g. " + f'`--{cls.options_scope}-args="--skip B101,B308 --confidence"`' + ), + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=( + "Path to a Bandit YAML config file " + "(https://bandit.readthedocs.io/en/latest/config.html)." + ), + ) @property def config_request(self) -> ConfigFilesRequest: diff --git a/src/python/pants/backend/python/lint/black/subsystem.py b/src/python/pants/backend/python/lint/black/subsystem.py index 591b122d9f2..050a80e1c6d 100644 --- a/src/python/pants/backend/python/lint/black/subsystem.py +++ b/src/python/pants/backend/python/lint/black/subsystem.py @@ -4,7 +4,7 @@ from __future__ import annotations import os.path -from typing import Iterable, cast +from typing import Iterable from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -18,7 +18,7 @@ from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import AllTargets, AllTargetsRequest from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str +from pants.option.option_types import ArgsListOption, BoolOption, FileOption from pants.util.docutil import bin_name, git_url from pants.util.logging import LogLevel @@ -39,59 +39,38 @@ class Black(PythonToolBase): default_lockfile_url = git_url(default_lockfile_path) default_extra_requirements = ['typing-extensions>=3.10.0.0; python_version < "3.10"'] - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use Black when running `{bin_name()} fmt` and `{bin_name()} lint`", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to Black, e.g. " - f'`--{cls.options_scope}-args="--target-version=py37 --quiet"`' - ), - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to a TOML config file understood by Black " - "(https://github.com/psf/black#configuration-format).\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - f"this option if the config is located in a non-standard location." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include any relevant pyproject.toml config files during runs." - f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " - f"non-standard location." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use Black when running `{bin_name()} fmt` and `{bin_name()} lint`", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to Black, e.g. " + f'`--{cls.options_scope}-args="--target-version=py37 --quiet"`' + ), + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=lambda cls: ( + "Path to a TOML config file understood by Black " + "(https://github.com/psf/black#configuration-format).\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + f"this option if the config is located in a non-standard location." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include any relevant pyproject.toml config files during runs." + f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " + f"non-standard location." + ), + ) def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: # Refer to https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#where-black-looks-for-the-file @@ -100,7 +79,7 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"[{self.options_scope}].config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_content=candidates, ) diff --git a/src/python/pants/backend/python/lint/docformatter/subsystem.py b/src/python/pants/backend/python/lint/docformatter/subsystem.py index 8392dc5afc6..cb811d0f789 100644 --- a/src/python/pants/backend/python/lint/docformatter/subsystem.py +++ b/src/python/pants/backend/python/lint/docformatter/subsystem.py @@ -1,7 +1,6 @@ # Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -from typing import Tuple, cast from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -10,7 +9,7 @@ from pants.core.goals.generate_lockfiles import GenerateToolLockfileSentinel from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule -from pants.option.custom_types import shell_str +from pants.option.option_types import ArgsListOption, BoolOption from pants.util.docutil import bin_name, git_url @@ -29,32 +28,17 @@ class Docformatter(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/lint/docformatter/lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use docformatter when running `{bin_name()} fmt` and `{bin_name()} lint`.", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to docformatter, e.g. " - f'`--{cls.options_scope}-args="--wrap-summaries=100 --pre-summary-newline"`.' - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> Tuple[str, ...]: - return tuple(self.options.args) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use docformatter when running `{bin_name()} fmt` and `{bin_name()} lint`.", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to docformatter, e.g. " + f'`--{cls.options_scope}-args="--wrap-summaries=100 --pre-summary-newline"`.' + ), + ) class DocformatterLockfileSentinel(GenerateToolLockfileSentinel): diff --git a/src/python/pants/backend/python/lint/flake8/subsystem.py b/src/python/pants/backend/python/lint/flake8/subsystem.py index ee2c76e916f..76bd1a87791 100644 --- a/src/python/pants/backend/python/lint/flake8/subsystem.py +++ b/src/python/pants/backend/python/lint/flake8/subsystem.py @@ -5,7 +5,6 @@ import itertools from dataclasses import dataclass -from typing import cast from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -40,7 +39,7 @@ TransitiveTargetsRequest, ) from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str, target_option +from pants.option.option_types import ArgsListOption, BoolOption, FileOption, TargetListOption from pants.util.docutil import bin_name, doc_url, git_url from pants.util.logging import LogLevel from pants.util.ordered_set import FrozenOrderedSet, OrderedSet @@ -70,91 +69,68 @@ class Flake8(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/lint/flake8/lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use Flake8 when running `{bin_name()} lint`", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to Flake8, e.g. " - f'`--{cls.options_scope}-args="--ignore E123,W456 --enable-extensions H111"`' - ), - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to an INI config file understood by Flake8 " - "(https://flake8.pycqa.org/en/latest/user/configuration.html).\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - f"this option if the config is located in a non-standard location." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include any relevant config files during " - "runs (`.flake8`, `flake8`, `setup.cfg`, and `tox.ini`)." - f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " - f"non-standard location." - ), - ) - register( - "--source-plugins", - type=list, - member_type=target_option, - advanced=True, - help=( - "An optional list of `python_sources` target addresses to load first-party " - "plugins.\n\nYou must set the plugin's parent directory as a source root. For " - "example, if your plugin is at `build-support/flake8/custom_plugin.py`, add " - "'build-support/flake8' to `[source].root_patterns` in `pants.toml`. This is " - "necessary for Pants to know how to tell Flake8 to discover your plugin. See " - f"{doc_url('source-roots')}\n\nYou must also set `[flake8:local-plugins]` in " - "your Flake8 config file. " - "For example:\n\n" - "```\n" - "[flake8:local-plugins]\n" - " extension =\n" - " CUSTOMCODE = custom_plugin:MyChecker\n" - "```\n\n" - "While your plugin's code can depend on other first-party code and third-party " - "requirements, all first-party dependencies of the plugin must live in the same " - "directory or a subdirectory.\n\n" - "To instead load third-party plugins, set the option " - "`[flake8].extra_requirements`.\n\n" - "Tip: it's often helpful to define a dedicated 'resolve' via " - "`[python].resolves` for your Flake8 plugins such as 'flake8-plugins' " - "so that the third-party requirements used by your plugin, like `flake8`, do not " - "mix with the rest of your project. Read that option's help message for more info " - "on resolves." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use Flake8 when running `{bin_name()} lint`", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to Flake8, e.g. " + f'`--{cls.options_scope}-args="--ignore E123,W456 --enable-extensions H111"`' + ), + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=lambda cls: ( + "Path to an INI config file understood by Flake8 " + "(https://flake8.pycqa.org/en/latest/user/configuration.html).\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + f"this option if the config is located in a non-standard location." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include any relevant config files during " + "runs (`.flake8`, `flake8`, `setup.cfg`, and `tox.ini`)." + f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " + f"non-standard location." + ), + ) + _source_plugins = TargetListOption( + "--source-plugins", + advanced=True, + help=( + "An optional list of `python_sources` target addresses to load first-party " + "plugins.\n\nYou must set the plugin's parent directory as a source root. For " + "example, if your plugin is at `build-support/flake8/custom_plugin.py`, add " + "'build-support/flake8' to `[source].root_patterns` in `pants.toml`. This is " + "necessary for Pants to know how to tell Flake8 to discover your plugin. See " + f"{doc_url('source-roots')}\n\nYou must also set `[flake8:local-plugins]` in " + "your Flake8 config file. " + "For example:\n\n" + "```\n" + "[flake8:local-plugins]\n" + " extension =\n" + " CUSTOMCODE = custom_plugin:MyChecker\n" + "```\n\n" + "While your plugin's code can depend on other first-party code and third-party " + "requirements, all first-party dependencies of the plugin must live in the same " + "directory or a subdirectory.\n\n" + "To instead load third-party plugins, set the option " + "`[flake8].extra_requirements`.\n\n" + "Tip: it's often helpful to define a dedicated 'resolve' via " + "`[python].resolves` for your Flake8 plugins such as 'flake8-plugins' " + "so that the third-party requirements used by your plugin, like `flake8`, do not " + "mix with the rest of your project. Read that option's help message for more info " + "on resolves." + ), + ) @property def config_request(self) -> ConfigFilesRequest: @@ -163,14 +139,14 @@ def config_request(self) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"[{self.options_scope}].config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_existence=["flake8", ".flake8"], check_content={"setup.cfg": b"[flake8]", "tox.ini": b"[flake8]"}, ) @property def source_plugins(self) -> UnparsedAddressInputs: - return UnparsedAddressInputs(self.options.source_plugins, owning_address=None) + return UnparsedAddressInputs(self._source_plugins, owning_address=None) # -------------------------------------------------------------------------------------- diff --git a/src/python/pants/backend/python/lint/isort/subsystem.py b/src/python/pants/backend/python/lint/isort/subsystem.py index 57f615c9355..a15a2b34b85 100644 --- a/src/python/pants/backend/python/lint/isort/subsystem.py +++ b/src/python/pants/backend/python/lint/isort/subsystem.py @@ -4,7 +4,7 @@ from __future__ import annotations import os.path -from typing import Iterable, cast +from typing import Iterable from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -14,7 +14,7 @@ from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str +from pants.option.option_types import ArgsListOption, BoolOption, FileListOption from pants.util.docutil import bin_name, git_url @@ -33,71 +33,49 @@ class Isort(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/lint/isort/lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use isort when running `{bin_name()} fmt` and `{bin_name()} lint`.", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to isort, e.g. " - f'`--{cls.options_scope}-args="--case-sensitive --trailing-comma"`.' - ), - ) - register( - "--config", - # TODO: Figure out how to deprecate this being a list in favor of a single string. - # Thanks to config autodiscovery, this option should only be used because you want - # Pants to explicitly set `--settings`, which only works w/ 1 config file. - # isort 4 users should instead use autodiscovery to support multiple config files. - # Deprecating this could be tricky, but should be possible thanks to the implicit - # add syntax. - # - # When deprecating, also deprecate the user manually setting `--settings` with - # `[isort].args`. - type=list, - member_type=file_option, - advanced=True, - help=( - "Path to config file understood by isort " - "(https://pycqa.github.io/isort/docs/configuration/config_files/).\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - f"this option if the config is located in a non-standard location.\n\n" - "If using isort 5+ and you specify only 1 config file, Pants will configure " - "isort's argv to point to your config file." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include any relevant config files during " - "runs (`.isort.cfg`, `pyproject.toml`, `setup.cfg`, `tox.ini` and `.editorconfig`)." - f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " - f"non-standard location." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def config(self) -> tuple[str, ...]: - return tuple(self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use isort when running `{bin_name()} fmt` and `{bin_name()} lint`.", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to isort, e.g. " + f'`--{cls.options_scope}-args="--case-sensitive --trailing-comma"`.' + ), + ) + config = FileListOption( + "--config", + # TODO: Figure out how to deprecate this being a list in favor of a single string. + # Thanks to config autodiscovery, this option should only be used because you want + # Pants to explicitly set `--settings`, which only works w/ 1 config file. + # isort 4 users should instead use autodiscovery to support multiple config files. + # Deprecating this could be tricky, but should be possible thanks to the implicit + # add syntax. + # + # When deprecating, also deprecate the user manually setting `--settings` with + # `[isort].args`. + advanced=True, + help=lambda cls: ( + "Path to config file understood by isort " + "(https://pycqa.github.io/isort/docs/configuration/config_files/).\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + f"this option if the config is located in a non-standard location.\n\n" + "If using isort 5+ and you specify only 1 config file, Pants will configure " + "isort's argv to point to your config file." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include any relevant config files during " + "runs (`.isort.cfg`, `pyproject.toml`, `setup.cfg`, `tox.ini` and `.editorconfig`)." + f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " + f"non-standard location." + ), + ) def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: # Refer to https://pycqa.github.io/isort/docs/configuration/config_files/. @@ -117,7 +95,7 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"[{self.options_scope}].config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_existence=check_existence, check_content=check_content, ) diff --git a/src/python/pants/backend/python/lint/pylint/subsystem.py b/src/python/pants/backend/python/lint/pylint/subsystem.py index d7da032e673..f79cc87355a 100644 --- a/src/python/pants/backend/python/lint/pylint/subsystem.py +++ b/src/python/pants/backend/python/lint/pylint/subsystem.py @@ -6,7 +6,7 @@ import itertools import os.path from dataclasses import dataclass -from typing import Iterable, cast +from typing import Iterable from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -42,7 +42,7 @@ TransitiveTargetsRequest, ) from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str, target_option +from pants.option.option_types import ArgsListOption, BoolOption, FileOption, TargetListOption from pants.util.docutil import bin_name, doc_url, git_url from pants.util.logging import LogLevel from pants.util.ordered_set import FrozenOrderedSet, OrderedSet @@ -78,86 +78,63 @@ class Pylint(PythonToolBase): default_lockfile_url = git_url(default_lockfile_path) uses_requirements_from_source_plugins = True - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use Pylint when running `{bin_name()} lint`", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to Pylint, e.g. " - f'`--{cls.options_scope}-args="--ignore=foo.py,bar.py --disable=C0330,W0311"`' - ), - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to a config file understood by Pylint " - "(http://pylint.pycqa.org/en/latest/user_guide/run.html#command-line-options).\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - f"this option if the config is located in a non-standard location." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include any relevant config files during " - "runs (`.pylintrc`, `pylintrc`, `pyproject.toml`, and `setup.cfg`)." - f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " - f"non-standard location." - ), - ) - register( - "--source-plugins", - type=list, - member_type=target_option, - advanced=True, - help=( - "An optional list of `python_sources` target addresses to load first-party " - "plugins.\n\nYou must set the plugin's parent directory as a source root. For " - "example, if your plugin is at `build-support/pylint/custom_plugin.py`, add " - "'build-support/pylint' to `[source].root_patterns` in `pants.toml`. This is " - "necessary for Pants to know how to tell Pylint to discover your plugin. See " - f"{doc_url('source-roots')}\n\n" - f"You must also set `load-plugins=$module_name` in your Pylint config file.\n\n" - "While your plugin's code can depend on other first-party code and third-party " - "requirements, all first-party dependencies of the plugin must live in the same " - "directory or a subdirectory.\n\n" - "To instead load third-party plugins, set the " - "option `[pylint].extra_requirements` and set the `load-plugins` option in your " - "Pylint config.\n\n" - "Tip: it's often helpful to define a dedicated 'resolve' via " - "`[python].resolves` for your Pylint plugins such as 'pylint-plugins' " - "so that the third-party requirements used by your plugin, like `pylint`, do not " - "mix with the rest of your project. Read that option's help message for more info " - "on resolves." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use Pylint when running `{bin_name()} lint`", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to Pylint, e.g. " + f'`--{cls.options_scope}-args="--ignore=foo.py,bar.py --disable=C0330,W0311"`' + ), + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=lambda cls: ( + "Path to a config file understood by Pylint " + "(http://pylint.pycqa.org/en/latest/user_guide/run.html#command-line-options).\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + f"this option if the config is located in a non-standard location." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include any relevant config files during " + "runs (`.pylintrc`, `pylintrc`, `pyproject.toml`, and `setup.cfg`)." + f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " + f"non-standard location." + ), + ) + _source_plugins = TargetListOption( + "--source-plugins", + advanced=True, + help=( + "An optional list of `python_sources` target addresses to load first-party " + "plugins.\n\nYou must set the plugin's parent directory as a source root. For " + "example, if your plugin is at `build-support/pylint/custom_plugin.py`, add " + "'build-support/pylint' to `[source].root_patterns` in `pants.toml`. This is " + "necessary for Pants to know how to tell Pylint to discover your plugin. See " + f"{doc_url('source-roots')}\n\n" + f"You must also set `load-plugins=$module_name` in your Pylint config file.\n\n" + "While your plugin's code can depend on other first-party code and third-party " + "requirements, all first-party dependencies of the plugin must live in the same " + "directory or a subdirectory.\n\n" + "To instead load third-party plugins, set the " + "option `[pylint].extra_requirements` and set the `load-plugins` option in your " + "Pylint config.\n\n" + "Tip: it's often helpful to define a dedicated 'resolve' via " + "`[python].resolves` for your Pylint plugins such as 'pylint-plugins' " + "so that the third-party requirements used by your plugin, like `pylint`, do not " + "mix with the rest of your project. Read that option's help message for more info " + "on resolves." + ), + ) def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: # Refer to http://pylint.pycqa.org/en/latest/user_guide/run.html#command-line-options for @@ -165,14 +142,14 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"[{self.options_scope}].config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_existence=[".pylinrc", *(os.path.join(d, "pylintrc") for d in ("", *dirs))], check_content={"pyproject.toml": b"[tool.pylint]", "setup.cfg": b"[pylint."}, ) @property def source_plugins(self) -> UnparsedAddressInputs: - return UnparsedAddressInputs(self.options.source_plugins, owning_address=None) + return UnparsedAddressInputs(self._source_plugins, owning_address=None) # -------------------------------------------------------------------------------------- diff --git a/src/python/pants/backend/python/lint/pyupgrade/subsystem.py b/src/python/pants/backend/python/lint/pyupgrade/subsystem.py index 03a9b0ee8f2..1f79f08beb7 100644 --- a/src/python/pants/backend/python/lint/pyupgrade/subsystem.py +++ b/src/python/pants/backend/python/lint/pyupgrade/subsystem.py @@ -3,8 +3,6 @@ from __future__ import annotations -from typing import cast - from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile from pants.backend.python.subsystems.python_tool_base import PythonToolBase @@ -12,7 +10,7 @@ from pants.core.goals.generate_lockfiles import GenerateToolLockfileSentinel from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule -from pants.option.custom_types import shell_str +from pants.option.option_types import ArgsListOption, BoolOption from pants.util.docutil import bin_name, git_url @@ -33,33 +31,17 @@ class PyUpgrade(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/lint/pyupgrade/lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use pyupgrade when running `{bin_name()} fmt` and `{bin_name()} lint`.", - ) - register( - "--args", - type=list, - default=[], - member_type=shell_str, - help=( - f"Arguments to pass directly to pyupgrade, e.g. " - f'`--{cls.options_scope}-args="--py39-plus --keep-runtime-typing"`' - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use pyupgrade when running `{bin_name()} fmt` and `{bin_name()} lint`.", + ) + args = ArgsListOption( + help=lambda cls: ( + f"Arguments to pass directly to pyupgrade, e.g. " + f'`--{cls.options_scope}-args="--py39-plus --keep-runtime-typing"`' + ), + ) class PyUpgradeLockfileSentinel(GenerateToolLockfileSentinel): diff --git a/src/python/pants/backend/python/lint/yapf/subsystem.py b/src/python/pants/backend/python/lint/yapf/subsystem.py index 134bbedb003..b7e37eef5c9 100644 --- a/src/python/pants/backend/python/lint/yapf/subsystem.py +++ b/src/python/pants/backend/python/lint/yapf/subsystem.py @@ -4,7 +4,7 @@ from __future__ import annotations import os.path -from typing import Iterable, cast +from typing import Iterable from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -14,7 +14,7 @@ from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str +from pants.option.option_types import ArgsListOption, BoolOption, FileOption from pants.util.docutil import bin_name, git_url @@ -34,63 +34,42 @@ class Yapf(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/lint/yapf/lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use yapf when running `{bin_name()} fmt` and `{bin_name()} lint`.", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to yapf, e.g. " - f'`--{cls.options_scope}-args="--no-local-style"`.\n\n' - "Certain arguments, specifically `--recursive`, `--in-place`, and " - "`--parallel`, will be ignored because Pants takes care of finding " - "all the relevant files and running the formatting in parallel." - ), - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to style file understood by yapf " - "(https://github.com/google/yapf#formatting-style/).\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - f"this option if the config is located in a non-standard location." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include any relevant config files during " - "runs (`.style.yapf`, `pyproject.toml`, and `setup.cfg`)." - f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " - f"non-standard location." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use yapf when running `{bin_name()} fmt` and `{bin_name()} lint`.", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to yapf, e.g. " + f'`--{cls.options_scope}-args="--no-local-style"`.\n\n' + "Certain arguments, specifically `--recursive`, `--in-place`, and " + "`--parallel`, will be ignored because Pants takes care of finding " + "all the relevant files and running the formatting in parallel." + ), + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=lambda cls: ( + "Path to style file understood by yapf " + "(https://github.com/google/yapf#formatting-style/).\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + f"this option if the config is located in a non-standard location." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include any relevant config files during " + "runs (`.style.yapf`, `pyproject.toml`, and `setup.cfg`)." + f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " + f"non-standard location." + ), + ) def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: # Refer to https://github.com/google/yapf#formatting-style. @@ -109,7 +88,7 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"[{self.options_scope}].config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.options.config_discovery, check_existence=check_existence, check_content=check_content, ) diff --git a/src/python/pants/backend/python/mixed_interpreter_constraints/py_constraints.py b/src/python/pants/backend/python/mixed_interpreter_constraints/py_constraints.py index 0b938b2ce5c..84589287f42 100644 --- a/src/python/pants/backend/python/mixed_interpreter_constraints/py_constraints.py +++ b/src/python/pants/backend/python/mixed_interpreter_constraints/py_constraints.py @@ -5,7 +5,6 @@ import logging from collections import defaultdict from textwrap import fill, indent -from typing import cast from pants.backend.project_info.dependees import Dependees, DependeesRequest from pants.backend.python.subsystems.setup import PythonSetup @@ -23,6 +22,7 @@ TransitiveTargetsRequest, ) from pants.engine.unions import UnionMembership +from pants.option.option_types import BoolOption from pants.util.docutil import bin_name logger = logging.getLogger(__name__) @@ -32,28 +32,20 @@ class PyConstraintsSubsystem(Outputting, GoalSubsystem): name = "py-constraints" help = "Determine what Python interpreter constraints are used by files/targets." - @classmethod - def register_options(cls, register) -> None: - super().register_options(register) - register( - "--summary", - type=bool, - default=False, - help=( - "Output a CSV summary of interpreter constraints for your whole repository. The " - "headers are `Target`, `Constraints`, `Transitive Constraints`, `# Dependencies`, " - "and `# Dependees`.\n\nThis information can be useful when prioritizing a " - "migration from one Python version to another (e.g. to Python 3). Use " - "`# Dependencies` and `# Dependees` to help prioritize which targets are easiest " - "to port (low # dependencies) and highest impact to port (high # dependees).\n\n" - "Use a tool like Pandas or Excel to process the CSV. Use the option " - "`--py-constraints-output-file=summary.csv` to write directly to a file." - ), - ) - - @property - def summary(self) -> bool: - return cast(bool, self.options.summary) + summary = BoolOption( + "--summary", + default=False, + help=( + "Output a CSV summary of interpreter constraints for your whole repository. The " + "headers are `Target`, `Constraints`, `Transitive Constraints`, `# Dependencies`, " + "and `# Dependees`.\n\nThis information can be useful when prioritizing a " + "migration from one Python version to another (e.g. to Python 3). Use " + "`# Dependencies` and `# Dependees` to help prioritize which targets are easiest " + "to port (low # dependencies) and highest impact to port (high # dependees).\n\n" + "Use a tool like Pandas or Excel to process the CSV. Use the option " + "`--py-constraints-output-file=summary.csv` to write directly to a file." + ), + ) class PyConstraintsGoal(Goal): diff --git a/src/python/pants/backend/python/subsystems/ipython.py b/src/python/pants/backend/python/subsystems/ipython.py index 1a2f4f50819..a87001a327f 100644 --- a/src/python/pants/backend/python/subsystems/ipython.py +++ b/src/python/pants/backend/python/subsystems/ipython.py @@ -15,6 +15,7 @@ from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import AllTargets, AllTargetsRequest from pants.engine.unions import UnionRule +from pants.option.option_types import BoolOption from pants.util.docutil import git_url from pants.util.logging import LogLevel @@ -31,21 +32,17 @@ class IPython(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/subsystems/ipython_lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--ignore-cwd", - type=bool, - advanced=True, - default=True, - help="Whether to tell IPython not to put the CWD on the import path.\n\n" - "Normally you want this to be True, so that imports come from the hermetic " - "environment Pants creates.\n\nHowever IPython<7.13.0 doesn't support this option, " - "so if you're using an earlier version (e.g., because you have Python 2.7 code) " - "then you will need to set this to False, and you may have issues with imports " - "from your CWD shading the hermetic environment.", - ) + ignore_cwd = BoolOption( + "--ignore-cwd", + advanced=True, + default=True, + help="Whether to tell IPython not to put the CWD on the import path.\n\n" + "Normally you want this to be True, so that imports come from the hermetic " + "environment Pants creates.\n\nHowever IPython<7.13.0 doesn't support this option, " + "so if you're using an earlier version (e.g., because you have Python 2.7 code) " + "then you will need to set this to False, and you may have issues with imports " + "from your CWD shading the hermetic environment.", + ) class IPythonLockfileSentinel(GenerateToolLockfileSentinel): diff --git a/src/python/pants/backend/python/subsystems/pytest.py b/src/python/pants/backend/python/subsystems/pytest.py index 7634750a637..3ab940579e5 100644 --- a/src/python/pants/backend/python/subsystems/pytest.py +++ b/src/python/pants/backend/python/subsystems/pytest.py @@ -6,7 +6,7 @@ import itertools import os.path from dataclasses import dataclass -from typing import Iterable, cast +from typing import Iterable from packaging.utils import canonicalize_name as canonicalize_project_name @@ -36,7 +36,7 @@ TransitiveTargetsRequest, ) from pants.engine.unions import UnionRule -from pants.option.custom_types import shell_str +from pants.option.option_types import ArgsListOption, BoolOption, IntOption, StrOption from pants.util.docutil import bin_name, doc_url, git_url from pants.util.logging import LogLevel from pants.util.memo import memoized_method @@ -73,89 +73,67 @@ class PyTest(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/subsystems/pytest_lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--args", - type=list, - member_type=shell_str, - passthrough=True, - help='Arguments to pass directly to Pytest, e.g. `--pytest-args="-k test_foo --quiet"`', - ) - register( - "--timeouts", - type=bool, - default=True, - help="Enable test target timeouts. If timeouts are enabled then test targets with a " - "timeout= parameter set on their target will time out after the given number of " - "seconds if not completed. If no timeout is set, then either the default timeout " - "is used or no timeout is configured.", - ) - register( - "--timeout-default", - type=int, - advanced=True, - help=( - "The default timeout (in seconds) for a test target if the `timeout` field is not " - "set on the target." - ), - ) - register( - "--timeout-maximum", - type=int, - advanced=True, - help="The maximum timeout (in seconds) that may be used on a `python_tests` target.", - ) - register( - "--junit-family", - type=str, - default="xunit2", - advanced=True, - help=( - "The format of generated junit XML files. See " - "https://docs.pytest.org/en/latest/reference.html#confval-junit_family." - ), - ) - register( - "--execution-slot-var", - type=str, - default=None, - advanced=True, - help=( - "If a non-empty string, the process execution slot id (an integer) will be exposed " - "to tests under this environment variable name." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include all relevant Pytest config files (e.g. `pytest.ini`) " - "during runs. See " - "https://docs.pytest.org/en/stable/customize.html#finding-the-rootdir for where " - "config files should be located for Pytest to discover them." - ), - ) + args = ArgsListOption( + passthrough=True, + help='Arguments to pass directly to Pytest, e.g. `--pytest-args="-k test_foo --quiet"`', + ) + timeouts_enabled = BoolOption( + "--timeouts", + default=True, + help="Enable test target timeouts. If timeouts are enabled then test targets with a " + "timeout= parameter set on their target will time out after the given number of " + "seconds if not completed. If no timeout is set, then either the default timeout " + "is used or no timeout is configured.", + ) + timeout_default = IntOption( + "--timeout-default", + default=None, + advanced=True, + help=( + "The default timeout (in seconds) for a test target if the `timeout` field is not " + "set on the target." + ), + ) + timeout_maximum = IntOption( + "--timeout-maximum", + default=None, + advanced=True, + help="The maximum timeout (in seconds) that may be used on a `python_tests` target.", + ) + juint_family = StrOption( + "--junit-family", + default="xunit2", + advanced=True, + help=( + "The format of generated junit XML files. See " + "https://docs.pytest.org/en/latest/reference.html#confval-junit_family." + ), + ) + execution_slot_var = StrOption( + "--execution-slot-var", + default=None, + advanced=True, + help=( + "If a non-empty string, the process execution slot id (an integer) will be exposed " + "to tests under this environment variable name." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=( + "If true, Pants will include all relevant Pytest config files (e.g. `pytest.ini`) " + "during runs. See " + "https://docs.pytest.org/en/stable/customize.html#finding-the-rootdir for where " + "config files should be located for Pytest to discover them." + ), + ) @property def all_requirements(self) -> tuple[str, ...]: return (self.version, *self.extra_requirements) - @property - def timeouts_enabled(self) -> bool: - return cast(bool, self.options.timeouts) - - @property - def timeout_default(self) -> int | None: - return cast("int | None", self.options.timeout_default) - - @property - def timeout_maximum(self) -> int | None: - return cast("int | None", self.options.timeout_maximum) - def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: # Refer to https://docs.pytest.org/en/stable/customize.html#finding-the-rootdir for how # config files are discovered. @@ -168,7 +146,7 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: check_content[os.path.join(d, "setup.cfg")] = b"[tool:pytest]" return ConfigFilesRequest( - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_existence=check_existence, check_content=check_content, ) diff --git a/src/python/pants/backend/python/subsystems/twine.py b/src/python/pants/backend/python/subsystems/twine.py index 533d811e8ab..3bc5a57d21d 100644 --- a/src/python/pants/backend/python/subsystems/twine.py +++ b/src/python/pants/backend/python/subsystems/twine.py @@ -5,7 +5,6 @@ import os from pathlib import Path -from typing import cast from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -16,7 +15,7 @@ from pants.engine.fs import CreateDigest, FileContent from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str +from pants.option.option_types import ArgsListOption, BoolOption, FileOption, StrOption from pants.util.docutil import bin_name, git_url @@ -41,69 +40,47 @@ class TwineSubsystem(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/subsystems/twine_lockfile.txt" default_lockfile_url = git_url(default_lockfile_path) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use Twine when running `{bin_name()} publish`.", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=("Arguments to pass directly to Twine, e.g. `--twine-args='--skip-existing'`.'"), - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to a .pypirc config file to use. " - "(https://packaging.python.org/specifications/pypirc/)\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - "this option if the config is located in a non-standard location." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include all relevant config files during runs " - "(`.pypirc`).\n\n" - f"Use `[{cls.options_scope}].config` instead if your config is in a " - "non-standard location." - ), - ) - register( - "--ca-certs-path", - advanced=True, - type=str, - default="", - help=( - "Path to a file containing PEM-format CA certificates used for verifying secure " - "connections when publishing python distributions.\n\n" - 'Uses the value from `[GLOBAL].ca_certs_path` by default. Set to `""` to ' - "not use the default CA certificate." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use Twine when running `{bin_name()} publish`.", + ) + args = ArgsListOption( + help=("Arguments to pass directly to Twine, e.g. `--twine-args='--skip-existing'`.'"), + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=lambda cls: ( + "Path to a .pypirc config file to use. " + "(https://packaging.python.org/specifications/pypirc/)\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + "this option if the config is located in a non-standard location." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include all relevant config files during runs " + "(`.pypirc`).\n\n" + f"Use `[{cls.options_scope}].config` instead if your config is in a " + "non-standard location." + ), + ) + ca_certs_path = StrOption( + "--ca-certs-path", + advanced=True, + default="", + help=( + "Path to a file containing PEM-format CA certificates used for verifying secure " + "connections when publishing python distributions.\n\n" + 'Uses the value from `[GLOBAL].ca_certs_path` by default. Set to `""` to ' + "not use the default CA certificate." + ), + ) def config_request(self) -> ConfigFilesRequest: # Refer to https://twine.readthedocs.io/en/latest/#configuration for how config files are @@ -111,12 +88,12 @@ def config_request(self) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"[{self.options_scope}].config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_existence=[".pypirc"], ) def ca_certs_digest_request(self, default_ca_certs_path: str | None) -> CreateDigest | None: - ca_certs_path: str | None = self.options.ca_certs_path + ca_certs_path: str | None = self.ca_certs_path if ca_certs_path == "": ca_certs_path = default_ca_certs_path if not ca_certs_path or ca_certs_path == "": diff --git a/src/python/pants/backend/python/typecheck/mypy/subsystem.py b/src/python/pants/backend/python/typecheck/mypy/subsystem.py index 4b2e288a17a..1edb046a78c 100644 --- a/src/python/pants/backend/python/typecheck/mypy/subsystem.py +++ b/src/python/pants/backend/python/typecheck/mypy/subsystem.py @@ -6,7 +6,7 @@ import itertools import logging from dataclasses import dataclass -from typing import Iterable, cast +from typing import Iterable from pants.backend.python.goals import lockfile from pants.backend.python.goals.lockfile import GeneratePythonLockfile @@ -38,7 +38,13 @@ TransitiveTargetsRequest, ) from pants.engine.unions import UnionRule -from pants.option.custom_types import file_option, shell_str, target_option +from pants.option.option_types import ( + ArgsListOption, + BoolOption, + FileOption, + StrListOption, + TargetListOption, +) from pants.util.docutil import bin_name, doc_url, git_url from pants.util.logging import LogLevel from pants.util.ordered_set import FrozenOrderedSet @@ -79,98 +85,69 @@ class MyPy(PythonToolBase): default_lockfile_url = git_url(default_lockfile_path) uses_requirements_from_source_plugins = True - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--skip", - type=bool, - default=False, - help=f"Don't use MyPy when running `{bin_name()} typecheck`.", - ) - register( - "--args", - type=list, - member_type=shell_str, - help=( - "Arguments to pass directly to mypy, e.g. " - f'`--{cls.options_scope}-args="--python-version 3.7 --disallow-any-expr"`' - ), - ) - register( - "--config", - type=file_option, - default=None, - advanced=True, - help=( - "Path to a config file understood by MyPy " - "(https://mypy.readthedocs.io/en/stable/config_file.html).\n\n" - f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " - f"this option if the config is located in a non-standard location." - ), - ) - register( - "--config-discovery", - type=bool, - default=True, - advanced=True, - help=( - "If true, Pants will include any relevant config files during " - "runs (`mypy.ini`, `.mypy.ini`, and `setup.cfg`)." - f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " - f"non-standard location." - ), - ) - register( - "--source-plugins", - type=list, - member_type=target_option, - advanced=True, - help=( - "An optional list of `python_sources` target addresses to load first-party " - "plugins.\n\n" - "You must also set `plugins = path.to.module` in your `mypy.ini`, and " - "set the `[mypy].config` option in your `pants.toml`.\n\n" - "To instead load third-party plugins, set the option `[mypy].extra_requirements` " - "and set the `plugins` option in `mypy.ini`." - "Tip: it's often helpful to define a dedicated 'resolve' via " - "`[python].resolves` for your MyPy plugins such as 'mypy-plugins' " - "so that the third-party requirements used by your plugin, like `mypy`, do not " - "mix with the rest of your project. Read that option's help message for more info " - "on resolves." - ), - ) - register( - "--extra-type-stubs", - type=list, - member_type=str, - advanced=True, - help=( - "Extra type stub requirements to install when running MyPy.\n\n" - "Normally, type stubs can be installed as typical requirements, such as putting " - "them in `requirements.txt` or using a `python_requirement` target." - "Alternatively, you can use this option so that the dependencies are solely " - "used when running MyPy and are not runtime dependencies.\n\n" - "Expects a list of pip-style requirement strings, like " - "`['types-requests==2.25.9']`." - ), - ) - - @property - def skip(self) -> bool: - return cast(bool, self.options.skip) - - @property - def args(self) -> tuple[str, ...]: - return tuple(self.options.args) - - @property - def extra_type_stubs(self) -> tuple[str, ...]: - return tuple(self.options.extra_type_stubs) - - @property - def config(self) -> str | None: - return cast("str | None", self.options.config) + skip = BoolOption( + "--skip", + default=False, + help=f"Don't use MyPy when running `{bin_name()} typecheck`.", + ) + args = ArgsListOption( + help=lambda cls: ( + "Arguments to pass directly to mypy, e.g. " + f'`--{cls.options_scope}-args="--python-version 3.7 --disallow-any-expr"`' + ), + ) + config = FileOption( + "--config", + default=None, + advanced=True, + help=lambda cls: ( + "Path to a config file understood by MyPy " + "(https://mypy.readthedocs.io/en/stable/config_file.html).\n\n" + f"Setting this option will disable `[{cls.options_scope}].config_discovery`. Use " + f"this option if the config is located in a non-standard location." + ), + ) + config_discovery = BoolOption( + "--config-discovery", + default=True, + advanced=True, + help=lambda cls: ( + "If true, Pants will include any relevant config files during " + "runs (`mypy.ini`, `.mypy.ini`, and `setup.cfg`)." + f"\n\nUse `[{cls.options_scope}].config` instead if your config is in a " + f"non-standard location." + ), + ) + _source_plugins = TargetListOption( + "--source-plugins", + advanced=True, + help=( + "An optional list of `python_sources` target addresses to load first-party " + "plugins.\n\n" + "You must also set `plugins = path.to.module` in your `mypy.ini`, and " + "set the `[mypy].config` option in your `pants.toml`.\n\n" + "To instead load third-party plugins, set the option `[mypy].extra_requirements` " + "and set the `plugins` option in `mypy.ini`." + "Tip: it's often helpful to define a dedicated 'resolve' via " + "`[python].resolves` for your MyPy plugins such as 'mypy-plugins' " + "so that the third-party requirements used by your plugin, like `mypy`, do not " + "mix with the rest of your project. Read that option's help message for more info " + "on resolves." + ), + ) + extra_type_stubs = StrListOption( + "--extra-type-stubs", + advanced=True, + help=( + "Extra type stub requirements to install when running MyPy.\n\n" + "Normally, type stubs can be installed as typical requirements, such as putting " + "them in `requirements.txt` or using a `python_requirement` target." + "Alternatively, you can use this option so that the dependencies are solely " + "used when running MyPy and are not runtime dependencies.\n\n" + "Expects a list of pip-style requirement strings, like " + "`['types-requests==2.25.9']`." + ), + ) @property def config_request(self) -> ConfigFilesRequest: @@ -178,14 +155,14 @@ def config_request(self) -> ConfigFilesRequest: return ConfigFilesRequest( specified=self.config, specified_option_name=f"{self.options_scope}.config", - discovery=cast(bool, self.options.config_discovery), + discovery=self.config_discovery, check_existence=["mypy.ini", ".mypy.ini"], check_content={"setup.cfg": b"[mypy", "pyproject.toml": b"[tool.mypy"}, ) @property def source_plugins(self) -> UnparsedAddressInputs: - return UnparsedAddressInputs(self.options.source_plugins, owning_address=None) + return UnparsedAddressInputs(self._source_plugins, owning_address=None) def check_and_warn_if_python_version_configured(self, config: FileContent | None) -> bool: """Determine if we can dynamically set `--python-version` and warn if not."""