diff --git a/pants-plugins/internal_plugins/releases/register.py b/pants-plugins/internal_plugins/releases/register.py index f9cc61a30c6..59500d0f442 100644 --- a/pants-plugins/internal_plugins/releases/register.py +++ b/pants-plugins/internal_plugins/releases/register.py @@ -21,9 +21,9 @@ from pants.engine.unions import UnionRule from pants.option.alias import CliAlias from pants.option.config import _ChainedConfig +from pants.option.option_types import DictOption from pants.option.options_bootstrapper import OptionsBootstrapper from pants.option.subsystem import Subsystem -from pants.util.frozendict import FrozenDict from pants.version import PANTS_SEMVER, VERSION @@ -31,18 +31,10 @@ class PantsReleases(Subsystem): options_scope = "pants-releases" help = "Options for Pants's release process." - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--release-notes", - type=dict, - help="A dict from branch name to release notes rst-file location.", - ) - - @property - def _release_notes(self) -> FrozenDict[str, str]: - return FrozenDict(self.options.release_notes) + _release_notes = DictOption[str]( + "--release-notes", + help="A dict from branch name to release notes rst-file location.", + ) @classmethod def _branch_name(cls, version: Version) -> str: diff --git a/src/python/pants/backend/docker/subsystems/docker_options.py b/src/python/pants/backend/docker/subsystems/docker_options.py index 17a566f67c6..a46bb308d4a 100644 --- a/src/python/pants/backend/docker/subsystems/docker_options.py +++ b/src/python/pants/backend/docker/subsystems/docker_options.py @@ -6,11 +6,17 @@ import os import sys from textwrap import dedent -from typing import cast +from typing import Any from pants.backend.docker.registries import DockerRegistries from pants.engine.environment import Environment -from pants.option.custom_types import shell_str, workspace_path +from pants.option.option_types import ( + DictOption, + ShellStrListOption, + StrListOption, + StrOption, + WorkspacePathOption, +) from pants.option.subsystem import Subsystem from pants.util.docutil import bin_name from pants.util.memo import memoized_method @@ -22,189 +28,153 @@ "https://docs.docker.com/engine/reference/commandline/cli/#environment-variables" ), } +registries_help = ( + dedent( + """\ + Configure Docker registries. The schema for a registry entry is as follows: + + { + "registry-alias": { + "address": "registry-domain:port", + "default": bool, + }, + ... + } + + """ + ) + + ( + "If no registries are provided in a `docker_image` target, then all default " + "addresses will be used, if any.\n" + "The `docker_image.registries` may be provided with a list of registry addresses " + "and registry aliases prefixed with `@` to be used instead of the defaults.\n" + "A configured registry is marked as default either by setting `default = true` " + 'or with an alias of `"default"`.' + ) +) +default_repository_help = ( + "Configure the default repository name used in the Docker image tag.\n\n" + "The value is formatted and may reference these variables (in addition to the normal " + "placeheolders derived from the Dockerfile and build args etc):\n\n" + + bullet_list(["name", "directory", "parent_directory"]) + + "\n\n" + 'Example: `--default-repository="{directory}/{name}"`.\n\n' + "The `name` variable is the `docker_image`'s target name, `directory` and " + "`parent_directory` are the name of the directory in which the BUILD file is for the " + "target, and its parent directory respectively.\n\n" + "Use the `repository` field to set this value directly on a `docker_image` " + "target.\nAny registries or tags are added to the image name as required, and should " + "not be part of the repository name." +) class DockerOptions(Subsystem): options_scope = "docker" help = "Options for interacting with Docker." - @classmethod - def register_options(cls, register): - registries_help = ( - dedent( - """\ - Configure Docker registries. The schema for a registry entry is as follows: - - { - "registry-alias": { - "address": "registry-domain:port", - "default": bool, - }, - ... - } - - """ - ) - + ( - "If no registries are provided in a `docker_image` target, then all default " - "addresses will be used, if any.\n" - "The `docker_image.registries` may be provided with a list of registry addresses " - "and registry aliases prefixed with `@` to be used instead of the defaults.\n" - "A configured registry is marked as default either by setting `default = true` " - 'or with an alias of `"default"`.' - ) - ) - default_repository_help = ( - "Configure the default repository name used in the Docker image tag.\n\n" - "The value is formatted and may reference these variables (in addition to the normal " - "placeheolders derived from the Dockerfile and build args etc):\n\n" - + bullet_list(["name", "directory", "parent_directory"]) - + "\n\n" - 'Example: `--default-repository="{directory}/{name}"`.\n\n' - "The `name` variable is the `docker_image`'s target name, `directory` and " - "`parent_directory` are the name of the directory in which the BUILD file is for the " - "target, and its parent directory respectively.\n\n" - "Use the `repository` field to set this value directly on a `docker_image` " - "target.\nAny registries or tags are added to the image name as required, and should " - "not be part of the repository name." - ) - super().register_options(register) - register("--registries", type=dict, fromfile=True, help=registries_help) - register( - "--default-repository", - type=str, - help=default_repository_help, - default="{name}", - ) - - register( - "--default-context-root", - type=workspace_path, - default="", - help=( - "Provide a default Docker build context root path for `docker_image` targets that " - "does not specify their own `context_root` field.\n\n" - "The context root is relative to the build root by default, but may be prefixed " - "with `./` to be relative to the directory of the BUILD file of the `docker_image`." - "\n\nExamples:\n\n" - " --default-context-root=src/docker\n" - " --default-context-root=./relative_to_the_build_file\n" - ), - ) - - register( - "--build-args", - type=list, - member_type=shell_str, - default=[], - help=( - "Global build arguments (for Docker `--build-arg` options) to use for all " - "`docker build` invocations.\n\n" - "Entries are either strings in the form `ARG_NAME=value` to set an explicit value; " - "or just `ARG_NAME` to copy the value from Pants's own environment.\n\n" - + dedent( - f"""\ + _registries = DictOption[Any]("--registries", help=registries_help).from_file() + default_repository = StrOption( + "--default-repository", + help=default_repository_help, + default="{name}", + ) + default_context_root = WorkspacePathOption( + "--default-context-root", + default="", + help=( + "Provide a default Docker build context root path for `docker_image` targets that " + "does not specify their own `context_root` field.\n\n" + "The context root is relative to the build root by default, but may be prefixed " + "with `./` to be relative to the directory of the BUILD file of the `docker_image`." + "\n\nExamples:\n\n" + " --default-context-root=src/docker\n" + " --default-context-root=./relative_to_the_build_file\n" + ), + ) + _build_args = ShellStrListOption( + "--build-args", + help=( + "Global build arguments (for Docker `--build-arg` options) to use for all " + "`docker build` invocations.\n\n" + "Entries are either strings in the form `ARG_NAME=value` to set an explicit value; " + "or just `ARG_NAME` to copy the value from Pants's own environment.\n\n" + + dedent( + f"""\ Example: - [{cls.options_scope}] + [{options_scope}] build_args = ["VAR1=value", "VAR2"] """ - ) - + "Use the `extra_build_args` field on a `docker_image` target for additional " - "image specific build arguments." - ), - ) - - register( - "--build-target-stage", - type=str, - help=( - "Global default value for `target_stage` on `docker_image` targets, overriding " - "the field value on the targets, if there is a matching stage in the `Dockerfile`." - "\n\n" - "This is useful to provide from the command line, to specify the target stage to " - "build for at execution time." - ), - ) - - register( - "--env-vars", - type=list, - member_type=shell_str, - default=[], - advanced=True, - help=( - "Environment variables to set for `docker` invocations.\n\n" - "Entries are either strings in the form `ENV_VAR=value` to set an explicit value; " - "or just `ENV_VAR` to copy the value from Pants's own environment." - ), - ) - - register( - "--run-args", - type=list, - member_type=shell_str, - default=["--interactive", "--tty"] if sys.stdout.isatty() else [], - help=( - "Additional arguments to use for `docker run` invocations.\n\n" - "Example:\n\n" - f' $ {bin_name()} run --{cls.options_scope}-run-args="-p 127.0.0.1:80:8080/tcp ' - '--name demo" src/example:image -- [image entrypoint args]\n\n' - "To provide the top-level options to the `docker` client, use " - f"`[{cls.options_scope}].env_vars` to configure the [Environment variables](" - f"{doc_links['docker_env_vars']}) as appropriate.\n\n" - "The arguments for the image entrypoint may be passed on the command line after a " - "double dash (`--`), or using the `--run-args` option.\n\n" - "Defaults to `--interactive --tty` when stdout is connected to a terminal." - ), - ) - - register( + ) + + "Use the `extra_build_args` field on a `docker_image` target for additional " + "image specific build arguments." + ), + ) + build_target_stage = StrOption( + "--build-target-stage", + help=( + "Global default value for `target_stage` on `docker_image` targets, overriding " + "the field value on the targets, if there is a matching stage in the `Dockerfile`." + "\n\n" + "This is useful to provide from the command line, to specify the target stage to " + "build for at execution time." + ), + ) + _env_vars = ShellStrListOption( + "--env-vars", + help=( + "Environment variables to set for `docker` invocations.\n\n" + "Entries are either strings in the form `ENV_VAR=value` to set an explicit value; " + "or just `ENV_VAR` to copy the value from Pants's own environment." + ), + ).advanced() + run_args = ShellStrListOption( + "--run-args", + default=["--interactive", "--tty"] if sys.stdout.isatty() else [], + help=( + "Additional arguments to use for `docker run` invocations.\n\n" + "Example:\n\n" + f' $ {bin_name()} run --{options_scope}-run-args="-p 127.0.0.1:80:8080/tcp ' + '--name demo" src/example:image -- [image entrypoint args]\n\n' + "To provide the top-level options to the `docker` client, use " + f"`[{options_scope}].env_vars` to configure the [Environment variables](" + f"{doc_links['docker_env_vars']}) as appropriate.\n\n" + "The arguments for the image entrypoint may be passed on the command line after a " + "double dash (`--`), or using the `--run-args` option.\n\n" + "Defaults to `--interactive --tty` when stdout is connected to a terminal." + ), + ) + _executable_search_paths = ( + StrListOption( "--executable-search-paths", - advanced=True, - type=list, default=[""], - metavar="", help=( "The PATH value that will be used to find the Docker client and any tools required." "\n\n" 'The special string `""` will expand to the contents of the PATH env var.' ), ) + .advanced() + .metavar("") + ) @property def build_args(self) -> tuple[str, ...]: - return tuple(sorted(set(self.options.build_args))) - - @property - def build_target_stage(self) -> str | None: - return cast("str | None", self.options.build_target_stage) - - @property - def run_args(self) -> tuple[str, ...]: - return tuple(self.options.run_args) + return tuple(sorted(set(self._build_args))) @property def env_vars(self) -> tuple[str, ...]: - return tuple(sorted(set(self.options.env_vars))) - - @property - def default_context_root(self) -> str: - return cast(str, self.options.default_context_root) - - @property - def default_repository(self) -> str: - return cast(str, self.options.default_repository) + return tuple(sorted(set(self._env_vars))) @memoized_method def registries(self) -> DockerRegistries: - return DockerRegistries.from_dict(self.options.registries) + return DockerRegistries.from_dict(self._registries) @memoized_method def executable_search_path(self, env: Environment) -> tuple[str, ...]: def iter_path_entries(): - for entry in self.options.executable_search_paths: + for entry in self._executable_search_paths: if entry == "": path = env.get("PATH") if path: diff --git a/src/python/pants/backend/java/subsystems/java_infer.py b/src/python/pants/backend/java/subsystems/java_infer.py index ba00623ad16..2ba1dafe6e0 100644 --- a/src/python/pants/backend/java/subsystems/java_infer.py +++ b/src/python/pants/backend/java/subsystems/java_infer.py @@ -1,7 +1,8 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -from typing import cast +from typing import Any +from pants.option.option_types import BoolOption, DictOption from pants.option.subsystem import Subsystem @@ -9,50 +10,27 @@ class JavaInferSubsystem(Subsystem): options_scope = "java-infer" help = "Options controlling which dependencies will be inferred for Java targets." - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--imports", - default=True, - type=bool, - help=("Infer a target's dependencies by parsing import statements from sources."), - ) - register( - "--consumed-types", - default=True, - type=bool, - help=("Infer a target's dependencies by parsing consumed types from sources."), - ) - register( - "--third-party-imports", - default=True, - type=bool, - help="Infer a target's third-party dependencies using Java import statements.", - ) - # TODO: Move to `coursier` or a generic `jvm` subsystem. - register( - "--third-party-import-mapping", - type=dict, - help=( - "A dictionary mapping a Java package path to a JVM artifact coordinate " - "(GROUP:ARTIFACT) without the version.\n\n" - "See `jvm_artifact` for more information on the mapping syntax." - ), - ) - - @property - def imports(self) -> bool: - return cast(bool, self.options.imports) - - @property - def consumed_types(self) -> bool: - return cast(bool, self.options.consumed_types) - - @property - def third_party_imports(self) -> bool: - return cast(bool, self.options.third_party_imports) - - @property - def third_party_import_mapping(self) -> dict: - return cast(dict, self.options.third_party_import_mapping) + imports = BoolOption( + "--imports", + default=True, + help=("Infer a target's dependencies by parsing import statements from sources."), + ) + consumed_types = BoolOption( + "--consumed-types", + default=True, + help=("Infer a target's dependencies by parsing consumed types from sources."), + ) + third_party_imports = BoolOption( + "--third-party-imports", + default=True, + help="Infer a target's third-party dependencies using Java import statements.", + ) + # TODO: Move to `coursier` or a generic `jvm` subsystem. + third_party_import_mapping = DictOption[Any]( + "--third-party-import-mapping", + help=( + "A dictionary mapping a Java package path to a JVM artifact coordinate " + "(GROUP:ARTIFACT) without the version.\n\n" + "See `jvm_artifact` for more information on the mapping syntax." + ), + ) diff --git a/src/python/pants/backend/project_info/regex_lint.py b/src/python/pants/backend/project_info/regex_lint.py index 34a049bb7ca..b4cec31b69c 100644 --- a/src/python/pants/backend/project_info/regex_lint.py +++ b/src/python/pants/backend/project_info/regex_lint.py @@ -8,7 +8,7 @@ import textwrap from dataclasses import dataclass from enum import Enum -from typing import Any, cast +from typing import Any from pants.base.exiter import PANTS_FAILED_EXIT_CODE, PANTS_SUCCEEDED_EXIT_CODE from pants.core.goals.lint import LintFilesRequest, LintResult, LintResults @@ -16,6 +16,7 @@ from pants.engine.fs import DigestContents, PathGlobs from pants.engine.rules import Get, collect_rules, rule from pants.engine.unions import UnionRule +from pants.option.option_types import DictOption, EnumOption from pants.option.subsystem import Subsystem from pants.util.frozendict import FrozenDict from pants.util.logging import LogLevel @@ -81,69 +82,56 @@ class RegexLintSubsystem(Subsystem): "`--changed-since=` does not yet cause this linter to run. We are exploring how to " "improve both these gotchas." ) - - @classmethod - def register_options(cls, register): - schema_help = textwrap.dedent( - """\ - ``` - { - 'required_matches': { - 'path_pattern1': [content_pattern1, content_pattern2], - 'path_pattern2': [content_pattern1, content_pattern3], - ... - }, - 'path_patterns': [ + schema_help = textwrap.dedent( + """\ + ``` { - 'name': path_pattern1', - 'pattern': , - 'inverted': True|False (defaults to False), - 'content_encoding': (defaults to utf8) + 'required_matches': { + 'path_pattern1': [content_pattern1, content_pattern2], + 'path_pattern2': [content_pattern1, content_pattern3], + ... }, - ... - ], - 'content_patterns': [ - { - 'name': 'content_pattern1', - 'pattern': , - 'inverted': True|False (defaults to False) + 'path_patterns': [ + { + 'name': path_pattern1', + 'pattern': , + 'inverted': True|False (defaults to False), + 'content_encoding': (defaults to utf8) + }, + ... + ], + 'content_patterns': [ + { + 'name': 'content_pattern1', + 'pattern': , + 'inverted': True|False (defaults to False) + } + ... + ] } - ... - ] - } - ``` - """ - ) - super().register_options(register) - register( - "--config", - type=dict, - fromfile=True, - help=( - f"Config schema is as follows:\n\n{schema_help}\n\n" - "Meaning: if a file matches some path pattern, its content must match all the " - "corresponding content patterns.\n\n" - "It's often helpful to load this config from a JSON or YAML file. To do that, set " - "`[regex-lint].config = '@path/to/config.yaml'`, for example." - ), - ) - register( - "--detail-level", - type=DetailLevel, - default=DetailLevel.nonmatching, - help="How much detail to include in the result.", - ) + ``` + """ + ) + + _config = DictOption[Any]( + "--config", + help=( + f"Config schema is as follows:\n\n{schema_help}\n\n" + "Meaning: if a file matches some path pattern, its content must match all the " + "corresponding content patterns.\n\n" + "It's often helpful to load this config from a JSON or YAML file. To do that, set " + "`[regex-lint].config = '@path/to/config.yaml'`, for example." + ), + ).from_file() + detail_level = EnumOption( + "--detail-level", + default=DetailLevel.nonmatching, + help="How much detail to include in the result.", + ) @memoized_method def get_multi_matcher(self) -> MultiMatcher | None: - return ( - MultiMatcher(ValidationConfig.from_dict(self.options.config)) - if self.options.config - else None - ) - - def detail_level(self) -> DetailLevel: - return cast(DetailLevel, self.options.detail_level) + return MultiMatcher(ValidationConfig.from_dict(self._config)) if self._config else None @dataclass(frozen=True) @@ -303,7 +291,7 @@ async def lint_with_regex_patterns( ) stdout = "" - detail_level = regex_lint_subsystem.detail_level() + detail_level = regex_lint_subsystem.detail_level num_matched_all = 0 num_nonmatched_some = 0 for rmr in regex_match_results: diff --git a/src/python/pants/backend/python/subsystems/setup.py b/src/python/pants/backend/python/subsystems/setup.py index 8e538b8ac2d..37220096386 100644 --- a/src/python/pants/backend/python/subsystems/setup.py +++ b/src/python/pants/backend/python/subsystems/setup.py @@ -8,7 +8,14 @@ import os from typing import Iterable, Iterator, Optional, cast -from pants.option.custom_types import file_option +from pants.option.option_types import ( + BoolOption, + DictOption, + EnumOption, + FileOption, + StrListOption, + StrOption, +) from pants.option.subsystem import Subsystem from pants.util.docutil import bin_name, doc_url from pants.util.memo import memoized_property @@ -30,15 +37,10 @@ class PythonSetup(Subsystem): default_interpreter_constraints = ["CPython>=3.6,<4"] default_interpreter_universe = ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"] - @classmethod - def register_options(cls, register): - super().register_options(register) - register( + interpreter_constraints = ( + StrListOption( "--interpreter-constraints", - advanced=True, - type=list, - default=PythonSetup.default_interpreter_constraints, - metavar="", + default=default_interpreter_constraints, help=( "The Python interpreters your codebase is compatible with.\n\nSpecify with " "requirement syntax, e.g. 'CPython>=2.7,<3' (A CPython interpreter with version " @@ -47,31 +49,30 @@ def register_options(cls, register): "default value for the `interpreter_constraints` field of Python targets." ), ) - register( - "--interpreter-versions-universe", - advanced=True, - type=list, - default=cls.default_interpreter_universe, - help=( - "All known Python major/minor interpreter versions that may be used by either " - "your code or tools used by your code.\n\n" - "This is used by Pants to robustly handle interpreter constraints, such as knowing " - "when generating lockfiles which Python versions to check if your code is " - "using.\n\n" - "This does not control which interpreter your code will use. Instead, to set your " - "interpreter constraints, update `[python].interpreter_constraints`, the " - "`interpreter_constraints` field, and relevant tool options like " - "`[isort].interpreter_constraints` to tell Pants which interpreters your code " - f"actually uses. See {doc_url('python-interpreter-compatibility')}.\n\n" - "All elements must be the minor and major Python version, e.g. '2.7' or '3.10'. Do " - "not include the patch version.\n\n" - ), - ) - register( + .advanced() + .metavar("") + ) + interpreter_universe = StrListOption( + "--interpreter-versions-universe", + default=default_interpreter_universe, + help=( + "All known Python major/minor interpreter versions that may be used by either " + "your code or tools used by your code.\n\n" + "This is used by Pants to robustly handle interpreter constraints, such as knowing " + "when generating lockfiles which Python versions to check if your code is " + "using.\n\n" + "This does not control which interpreter your code will use. Instead, to set your " + "interpreter constraints, update `[python].interpreter_constraints`, the " + "`interpreter_constraints` field, and relevant tool options like " + "`[isort].interpreter_constraints` to tell Pants which interpreters your code " + f"actually uses. See {doc_url('python-interpreter-compatibility')}.\n\n" + "All elements must be the minor and major Python version, e.g. '2.7' or '3.10'. Do " + "not include the patch version.\n\n" + ), + ).advanced() + requirement_constraints = ( + FileOption( "--requirement-constraints", - advanced=True, - type=file_option, - mutually_exclusive_group="lockfile", help=( "When resolving third-party requirements for your own code (vs. tools you run), " "use this constraints file to determine which versions to use.\n\n" @@ -84,207 +85,160 @@ def register_options(cls, register): "Mutually exclusive with `[python].enable_resolves`." ), ) - register( - "--resolve-all-constraints", - advanced=True, - default=True, - type=bool, - help=( - "If enabled, when resolving requirements, Pants will first resolve your entire " - "constraints file as a single global resolve. Then, if the code uses a subset of " - "your constraints file, Pants will extract the relevant requirements from that " - "global resolve so that only what's actually needed gets used. If disabled, Pants " - "will not use a global resolve and will resolve each subset of your requirements " - "independently." - "\n\nUsually this option should be enabled because it can result in far fewer " - "resolves." - "\n\nRequires [python].requirement_constraints to be set." - ), - ) - register( + .advanced() + .mutually_exclusive_group("lockfile") + ) + resolve_all_constraints = BoolOption( + "--resolve-all-constraints", + default=True, + help=( + "If enabled, when resolving requirements, Pants will first resolve your entire " + "constraints file as a single global resolve. Then, if the code uses a subset of " + "your constraints file, Pants will extract the relevant requirements from that " + "global resolve so that only what's actually needed gets used. If disabled, Pants " + "will not use a global resolve and will resolve each subset of your requirements " + "independently." + "\n\nUsually this option should be enabled because it can result in far fewer " + "resolves." + "\n\nRequires [python].requirement_constraints to be set." + ), + ).advanced() + enable_resolves = ( + BoolOption( "--enable-resolves", - advanced=True, - type=bool, default=False, - mutually_exclusive_group="lockfile", help=( "Set to true to enable the multiple resolves mechanism. See " "`[python].resolves` for an explanation of this feature.\n\n" "Mutually exclusive with `[python].requirement_constraints`." ), ) - register( - "--resolves", - advanced=True, - type=dict, - default={"python-default": "3rdparty/python/default_lock.txt"}, - help=( - "A mapping of logical names to lockfile paths used in your project.\n\n" - "Many organizations only need a single resolve for their whole project, which is " - "a good default and the simplest thing to do. However, you may need multiple " - "resolves, such as if you use two conflicting versions of a requirement in " - "your repository.\n\n" - "For now, Pants only has first-class support for disjoint resolves, meaning that " - "you cannot ergonomically set a `python_requirement` or `python_source` target, " - "for example, to work with multiple resolves. Practically, this means that you " - "cannot yet ergonomically reuse common code, such as util files, across projects " - "using different resolves. Support for overlapping resolves is coming soon.\n\n" - f"If you only need a single resolve, run `{bin_name()} generate-lockfiles` to " - "generate the lockfile.\n\n" - "If you need multiple resolves:\n\n" - " 1. Via this option, define multiple resolve " - "names and their lockfile paths. The names should be meaningful to your " - "repository, such as `data-science` or `pants-plugins`.\n" - " 2. Set the default with `[python].default_resolve`.\n" - " 3. Update your `python_requirement` targets with the " - "`resolve` field to declare which resolve they should " - "be available in. They default to `[python].default_resolve`, so you " - "only need to update targets that you want in non-default resolves. " - "(Often you'll set this via the `python_requirements` or `poetry_requirements` " - "target generators)\n" - f" 4. Run `{bin_name()} generate-lockfiles` to generate the lockfiles. If the results " - "aren't what you'd expect, adjust the prior step.\n" - " 5. Update any targets like `python_source` / `python_sources`, " - "`python_test` / `python_tests`, and `pex_binary` which need to set a non-default " - "resolve with the `resolve` field.\n\n" - "Only applies if `[python].enable_resolves` is true." - ), - ) - register( - "--default-resolve", - advanced=True, - type=str, - default="python-default", - help=( - "The default value used for the `resolve` field.\n\n" - "The name must be defined as a resolve in `[python].resolves`." - ), - ) - register( - "--resolves-to-interpreter-constraints", - advanced=True, - type=dict, - default={}, - help=( - "Override the interpreter constraints to use when generating a resolve's lockfile " - "with the `generate-lockfiles` goal.\n\n" - "By default, each resolve from `[python].resolves` will use your " - "global interpreter constraints set in `[python].interpreter_constraints`. With " - "this option, you can override each resolve to use certain interpreter " - "constraints, such as `{'data-science': ['==3.8.*']}`.\n\n" - "Pants will validate that the interpreter constraints of your code using a " - "resolve are compatible with that resolve's own constraints. For example, if your " - "code is set to use ['==3.9.*'] via the `interpreter_constraints` field, but it's " - "also using a resolve whose interpreter constraints are set to ['==3.7.*'], then " - "Pants will error explaining the incompatibility.\n\n" - "The keys must be defined as resolves in `[python].resolves`." - ), - ) - register( - "--invalid-lockfile-behavior", - advanced=True, - type=InvalidLockfileBehavior, - default=InvalidLockfileBehavior.error, - help=( - "The behavior when a lockfile has requirements or interpreter constraints that are " - "not compatible with what the current build is using.\n\n" - "We recommend keeping the default of `error` for CI builds." - ), - ) - register( - "--run-against-entire-lockfile", - advanced=True, - default=False, - type=bool, - help=( - "If enabled, when running binaries, tests, and repls, Pants will use the entire " - "lockfile/constraints file instead of just the relevant subset. This can improve " - "performance and reduce cache size, but has two consequences: 1) All cached test " - "results will be invalidated if any requirement in the lockfile changes, rather " - "than just those that depend on the changed requirement. 2) Requirements unneeded " - "by a test/run/repl will be present on the sys.path, which might in rare cases " - "cause their behavior to change.\n\n" - "This option does not affect packaging deployable artifacts, such as " - "PEX files, wheels and cloud functions, which will still use just the exact " - "subset of requirements needed." - ), - ) - register( - "--resolver-manylinux", - advanced=True, - type=str, - default="manylinux2014", - help="Whether to allow resolution of manylinux wheels when resolving requirements for " - "foreign linux platforms. The value should be a manylinux platform upper bound, " - "e.g.: 'manylinux2010', or else the string 'no' to disallow.", - ) - - register( - "--tailor-ignore-solitary-init-files", - type=bool, - default=True, - advanced=True, - help="Don't tailor `python_sources` targets for solitary `__init__.py` files, as " - "those usually exist as import scaffolding rather than true library code.\n\n" - "Set to False if you commonly have packages containing real code in " - "`__init__.py` and there are no other .py files in the package.", - ) - - register( - "--tailor-requirements-targets", - type=bool, - default=True, - advanced=True, - help="Tailor python_requirements() targets for requirements files.", - ) - - register( - "--tailor-pex-binary-targets", - type=bool, - default=True, - advanced=True, - help="Tailor pex_binary() targets for Python entry point files.", - ) - - register( - "--macos-big-sur-compatibility", - type=bool, - default=False, - help="If set, and if running on MacOS Big Sur, use macosx_10_16 as the platform " - "when building wheels. Otherwise, the default of macosx_11_0 will be used. " - "This may be required for pip to be able to install the resulting distribution " - "on Big Sur.", - ) - - @property - def interpreter_constraints(self) -> tuple[str, ...]: - return tuple(self.options.interpreter_constraints) - - @property - def interpreter_universe(self) -> tuple[str, ...]: - return tuple(self.options.interpreter_versions_universe) - - @property - def requirement_constraints(self) -> str | None: - """Path to constraint file.""" - return cast("str | None", self.options.requirement_constraints) - - @property - def enable_resolves(self) -> bool: - return cast(bool, self.options.enable_resolves) - - @property - def resolves(self) -> dict[str, str]: - return cast("dict[str, str]", self.options.resolves) - - @property - def default_resolve(self) -> str: - return cast(str, self.options.default_resolve) + .advanced() + .mutually_exclusive_group("lockfile") + ) + resolves = DictOption[str]( + "--resolves", + default={"python-default": "3rdparty/python/default_lock.txt"}, + help=( + "A mapping of logical names to lockfile paths used in your project.\n\n" + "Many organizations only need a single resolve for their whole project, which is " + "a good default and the simplest thing to do. However, you may need multiple " + "resolves, such as if you use two conflicting versions of a requirement in " + "your repository.\n\n" + "For now, Pants only has first-class support for disjoint resolves, meaning that " + "you cannot ergonomically set a `python_requirement` or `python_source` target, " + "for example, to work with multiple resolves. Practically, this means that you " + "cannot yet ergonomically reuse common code, such as util files, across projects " + "using different resolves. Support for overlapping resolves is coming soon.\n\n" + f"If you only need a single resolve, run `{bin_name()} generate-lockfiles` to " + "generate the lockfile.\n\n" + "If you need multiple resolves:\n\n" + " 1. Via this option, define multiple resolve " + "names and their lockfile paths. The names should be meaningful to your " + "repository, such as `data-science` or `pants-plugins`.\n" + " 2. Set the default with `[python].default_resolve`.\n" + " 3. Update your `python_requirement` targets with the " + "`resolve` field to declare which resolve they should " + "be available in. They default to `[python].default_resolve`, so you " + "only need to update targets that you want in non-default resolves. " + "(Often you'll set this via the `python_requirements` or `poetry_requirements` " + "target generators)\n" + f" 4. Run `{bin_name()} generate-lockfiles` to generate the lockfiles. If the results " + "aren't what you'd expect, adjust the prior step.\n" + " 5. Update any targets like `python_source` / `python_sources`, " + "`python_test` / `python_tests`, and `pex_binary` which need to set a non-default " + "resolve with the `resolve` field.\n\n" + "Only applies if `[python].enable_resolves` is true." + ), + ).advanced() + default_resolve = StrOption( + "--default-resolve", + default="python-default", + help=( + "The default value used for the `resolve` field.\n\n" + "The name must be defined as a resolve in `[python].resolves`." + ), + ).advanced() + _resolves_to_interpreter_constraints = DictOption["list[str]"]( + "--resolves-to-interpreter-constraints", + help=( + "Override the interpreter constraints to use when generating a resolve's lockfile " + "with the `generate-lockfiles` goal.\n\n" + "By default, each resolve from `[python].resolves` will use your " + "global interpreter constraints set in `[python].interpreter_constraints`. With " + "this option, you can override each resolve to use certain interpreter " + "constraints, such as `{'data-science': ['==3.8.*']}`.\n\n" + "Pants will validate that the interpreter constraints of your code using a " + "resolve are compatible with that resolve's own constraints. For example, if your " + "code is set to use ['==3.9.*'] via the `interpreter_constraints` field, but it's " + "also using a resolve whose interpreter constraints are set to ['==3.7.*'], then " + "Pants will error explaining the incompatibility.\n\n" + "The keys must be defined as resolves in `[python].resolves`." + ), + ).advanced() + invalid_lockfile_behavior = EnumOption( + "--invalid-lockfile-behavior", + default=InvalidLockfileBehavior.error, + help=( + "The behavior when a lockfile has requirements or interpreter constraints that are " + "not compatible with what the current build is using.\n\n" + "We recommend keeping the default of `error` for CI builds." + ), + ).advanced() + run_against_entire_lockfile = BoolOption( + "--run-against-entire-lockfile", + default=False, + help=( + "If enabled, when running binaries, tests, and repls, Pants will use the entire " + "lockfile/constraints file instead of just the relevant subset. This can improve " + "performance and reduce cache size, but has two consequences: 1) All cached test " + "results will be invalidated if any requirement in the lockfile changes, rather " + "than just those that depend on the changed requirement. 2) Requirements unneeded " + "by a test/run/repl will be present on the sys.path, which might in rare cases " + "cause their behavior to change.\n\n" + "This option does not affect packaging deployable artifacts, such as " + "PEX files, wheels and cloud functions, which will still use just the exact " + "subset of requirements needed." + ), + ).advanced() + resolver_manylinux = StrOption( + "--resolver-manylinux", + default="manylinux2014", + help="Whether to allow resolution of manylinux wheels when resolving requirements for " + "foreign linux platforms. The value should be a manylinux platform upper bound, " + "e.g.: 'manylinux2010', or else the string 'no' to disallow.", + ).advanced() + tailor_ignore_solitary_init_files = BoolOption( + "--tailor-ignore-solitary-init-files", + default=True, + help="Don't tailor `python_sources` targets for solitary `__init__.py` files, as " + "those usually exist as import scaffolding rather than true library code.\n\n" + "Set to False if you commonly have packages containing real code in " + "`__init__.py` and there are no other .py files in the package.", + ).advanced() + tailor_requirements_targets = BoolOption( + "--tailor-requirements-targets", + default=True, + help="Tailor python_requirements() targets for requirements files.", + ).advanced() + tailor_pex_binary_targets = BoolOption( + "--tailor-pex-binary-targets", + default=True, + help="Tailor pex_binary() targets for Python entry point files.", + ).advanced() + macos_big_sur_compatibility = BoolOption( + "--macos-big-sur-compatibility", + default=False, + help="If set, and if running on MacOS Big Sur, use macosx_10_16 as the platform " + "when building wheels. Otherwise, the default of macosx_11_0 will be used. " + "This may be required for pip to be able to install the resulting distribution " + "on Big Sur.", + ) @memoized_property def resolves_to_interpreter_constraints(self) -> dict[str, tuple[str, ...]]: result = {} - for resolve, ics in self.options.resolves_to_interpreter_constraints.items(): + for resolve, ics in self._resolves_to_interpreter_constraints.items(): if resolve not in self.resolves: raise KeyError( "Unrecognized resolve name in the option " @@ -295,24 +249,12 @@ def resolves_to_interpreter_constraints(self) -> dict[str, tuple[str, ...]]: result[resolve] = tuple(ics) return result - @property - def invalid_lockfile_behavior(self) -> InvalidLockfileBehavior: - return cast(InvalidLockfileBehavior, self.options.invalid_lockfile_behavior) - - @property - def run_against_entire_lockfile(self) -> bool: - return cast(bool, self.options.run_against_entire_lockfile) - - @property - def resolve_all_constraints(self) -> bool: - return cast(bool, self.options.resolve_all_constraints) - def resolve_all_constraints_was_set_explicitly(self) -> bool: return not self.options.is_default("resolve_all_constraints") @property def manylinux(self) -> str | None: - manylinux = cast(Optional[str], self.options.resolver_manylinux) + manylinux = cast(Optional[str], self.resolver_manylinux) if manylinux is None or manylinux.lower() in ("false", "no", "none"): return None return manylinux @@ -325,22 +267,6 @@ def manylinux_pex_args(self) -> Iterator[str]: else: yield "--no-manylinux" - @property - def tailor_ignore_solitary_init_files(self) -> bool: - return cast(bool, self.options.tailor_ignore_solitary_init_files) - - @property - def tailor_requirements_targets(self) -> bool: - return cast(bool, self.options.tailor_requirements_targets) - - @property - def tailor_pex_binary_targets(self) -> bool: - return cast(bool, self.options.tailor_pex_binary_targets) - - @property - def macos_big_sur_compatibility(self) -> bool: - return cast(bool, self.options.macos_big_sur_compatibility) - @property def scratch_dir(self): return os.path.join(self.options.pants_workdir, *self.options_scope.split(".")) diff --git a/src/python/pants/backend/scala/subsystems/scala.py b/src/python/pants/backend/scala/subsystems/scala.py index abcb82cfed3..aba7a3d7d7c 100644 --- a/src/python/pants/backend/scala/subsystems/scala.py +++ b/src/python/pants/backend/scala/subsystems/scala.py @@ -4,8 +4,8 @@ from __future__ import annotations import logging -from typing import cast +from pants.option.option_types import DictOption from pants.option.subsystem import Subsystem DEFAULT_SCALA_VERSION = "2.13.6" @@ -17,27 +17,18 @@ class ScalaSubsystem(Subsystem): options_scope = "scala" help = "Scala programming language" - @classmethod - def register_options(cls, register): - super().register_options(register) - - register( - "--version-for-resolve", - type=dict, - help=( - "A dictionary mapping the name of a resolve to the Scala version to use for all Scala " - "targets consuming that resolve.\n\n" - 'All Scala-compiled jars on a resolve\'s classpath must be "compatible" with one another and ' - "with all Scala-compiled first-party sources from `scala_sources` (and other Scala target types) " - "using that resolve. The option sets the Scala version that will be used to compile all " - "first-party sources using the resolve. This ensures that the compatibility property is " - "maintained for a resolve. To support multiple Scala versions, use multiple resolves." - ), - ) - - @property - def _version_for_resolve(self) -> dict[str, str]: - return cast("dict[str, str]", self.options.version_for_resolve) + _version_for_resolve = DictOption[str]( + "--version-for-resolve", + help=( + "A dictionary mapping the name of a resolve to the Scala version to use for all Scala " + "targets consuming that resolve.\n\n" + 'All Scala-compiled jars on a resolve\'s classpath must be "compatible" with one another and ' + "with all Scala-compiled first-party sources from `scala_sources` (and other Scala target types) " + "using that resolve. The option sets the Scala version that will be used to compile all " + "first-party sources using the resolve. This ensures that the compatibility property is " + "maintained for a resolve. To support multiple Scala versions, use multiple resolves." + ), + ) def version_for_resolve(self, resolve: str) -> str: version = self._version_for_resolve.get(resolve) diff --git a/src/python/pants/core/goals/tailor.py b/src/python/pants/core/goals/tailor.py index ba60d04b21f..460b83e3677 100644 --- a/src/python/pants/core/goals/tailor.py +++ b/src/python/pants/core/goals/tailor.py @@ -9,7 +9,7 @@ from abc import ABCMeta from collections import defaultdict from dataclasses import dataclass -from typing import Iterable, Iterator, Mapping, Optional, cast +from typing import Iterable, Iterator, Mapping, cast from pants.base.specs import AddressSpecs, AscendantAddresses, Spec, Specs from pants.build_graph.address import Address @@ -37,6 +37,7 @@ UnexpandedTargets, ) from pants.engine.unions import UnionMembership, union +from pants.option.option_types import BoolOption, DictOption, StrListOption, StrOption from pants.source.filespec import Filespec, matches_filespec from pants.util.docutil import bin_name, doc_url from pants.util.frozendict import FrozenDict @@ -245,112 +246,72 @@ class TailorSubsystem(GoalSubsystem): required_union_implementations = (PutativeTargetsRequest,) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--check", - type=bool, - default=False, - help=( - "Do not write changes to disk, only write back what would change. Return code " - "0 means there would be no changes, and 1 means that there would be. " - ), - ) - - register( - "--build-file-name", - advanced=True, - type=str, - default="BUILD", - help=( - "The name to use for generated BUILD files.\n\n" - "This must be compatible with `[GLOBAL].build_patterns`." - ), - ) - register( - "--build-file-header", - advanced=True, - type=str, - default=None, - help="A header, e.g., a copyright notice, to add to the content of created BUILD files.", - ) - register( - "--build-file-indent", - advanced=True, - type=str, - default=" ", - help="The indent to use when auto-editing BUILD files.", - ) - - register( - "--alias-mapping", - advanced=True, - type=dict, - help="A mapping from standard target type to custom type to use instead. The custom " + check = BoolOption( + "--check", + default=False, + help=( + "Do not write changes to disk, only write back what would change. Return code " + "0 means there would be no changes, and 1 means that there would be. " + ), + ) + build_file_name = StrOption( + "--build-file-name", + default="BUILD", + help=( + "The name to use for generated BUILD files.\n\n" + "This must be compatible with `[GLOBAL].build_patterns`." + ), + ).advanced() + build_file_header = StrOption( + "--build-file-header", + help="A header, e.g., a copyright notice, to add to the content of created BUILD files.", + ).advanced() + build_file_indent = StrOption( + "--build-file-indent", + default=" ", + help="The indent to use when auto-editing BUILD files.", + ).advanced() + _alias_mapping = DictOption[str]( + "--alias-mapping", + help=( + "A mapping from standard target type to custom type to use instead. The custom " "type can be a custom target type or a macro that offers compatible functionality " - f"to the one it replaces (see {doc_url('macros')}).", - ) - - register( - "--ignore-paths", - advanced=True, - type=list, - member_type=str, - help=( - "Do not edit or create BUILD files at these paths.\n\n" - "Can use literal file names and/or globs, e.g. " - "`['project/BUILD, 'ignore_me/**']`.\n\n" - "This augments the option `[GLOBAL].build_ignore`, which tells Pants to also not " - "_read_ BUILD files at certain paths. In contrast, this option only tells Pants to " - "not edit/create BUILD files at the specified paths." - ), - ) - register( - "--ignore-adding-targets", - advanced=True, - type=list, - member_type=str, - help=( - "Do not add these target definitions.\n\n" - "Expects a list of target addresses that would normally be added by `tailor`, " - "e.g. `['project:tgt']`. To find these names, you can run `tailor --check`, then " - "combine the BUILD file path with the target's name. For example, if `tailor` " - "would add the target `bin` to `project/BUILD`, then the address would be " - "`project:bin`. If the BUILD file is at the root of your repository, use `//` for " - "the path, e.g. `//:bin`.\n\n" - "Does not work with macros." - ), - ) - - @property - def check(self) -> bool: - return cast(bool, self.options.check) - - @property - def build_file_name(self) -> str: - return cast(str, self.options.build_file_name) - - @property - def build_file_header(self) -> str | None: - return cast(Optional[str], self.options.build_file_header) - - @property - def build_file_indent(self) -> str: - return cast(str, self.options.build_file_indent) - - @property - def ignore_paths(self) -> tuple[str, ...]: - return tuple(self.options.ignore_paths) + f"to the one it replaces (see {doc_url('macros')})." + ), + ).advanced() + ignore_paths = StrListOption( + "--ignore-paths", + help=( + "Do not edit or create BUILD files at these paths.\n\n" + "Can use literal file names and/or globs, e.g. " + "`['project/BUILD, 'ignore_me/**']`.\n\n" + "This augments the option `[GLOBAL].build_ignore`, which tells Pants to also not " + "_read_ BUILD files at certain paths. In contrast, this option only tells Pants to " + "not edit/create BUILD files at the specified paths." + ), + ).advanced() + _ignore_adding_targets = StrListOption( + "--ignore-adding-targets", + help=( + "Do not add these target definitions.\n\n" + "Expects a list of target addresses that would normally be added by `tailor`, " + "e.g. `['project:tgt']`. To find these names, you can run `tailor --check`, then " + "combine the BUILD file path with the target's name. For example, if `tailor` " + "would add the target `bin` to `project/BUILD`, then the address would be " + "`project:bin`. If the BUILD file is at the root of your repository, use `//` for " + "the path, e.g. `//:bin`.\n\n" + "Does not work with macros." + ), + ).advanced() @property def ignore_adding_targets(self) -> set[str]: - return set(self.options.ignore_adding_targets) + return set(self._ignore_adding_targets) def alias_for(self, standard_type: str) -> str | None: # The get() could return None, but casting to str | None errors. # This cast suffices to avoid typecheck errors. - return cast(str, self.options.alias_mapping.get(standard_type)) + return cast(str, self._alias_mapping.get(standard_type)) def validate_build_file_name(self, build_file_patterns: tuple[str, ...]) -> None: """Check that the specified BUILD file name works with the repository's BUILD file diff --git a/src/python/pants/jvm/subsystems.py b/src/python/pants/jvm/subsystems.py index c1213b7117a..05a8a8b6093 100644 --- a/src/python/pants/jvm/subsystems.py +++ b/src/python/pants/jvm/subsystems.py @@ -3,10 +3,9 @@ from __future__ import annotations -from typing import cast - from pants.engine.target import InvalidFieldException, Target from pants.jvm.target_types import JvmResolveField +from pants.option.option_types import DictOption, StrListOption, StrOption from pants.option.subsystem import Subsystem @@ -21,74 +20,48 @@ class JvmSubsystem(Subsystem): " will be whatever happens to be found first on the system's PATH." ) - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--tool-jdk", - default="adopt:1.11", - advanced=True, - help=( - "The JDK to use when building and running Pants' internal JVM support code and other " - "non-compiler tools. See `jvm` help for supported values." - ), - ) - register( - "--jdk", - type=str, - default="adopt:1.11", - help=( - "The default JDK to use when compiling sources or running tests for your code.\n\n" - "See `jvm` help for supported values." - ), - ) - register( - "--resolves", - type=dict, - default={"jvm-default": "3rdparty/jvm/default.lock"}, - # TODO: expand help message - help="A dictionary mapping resolve names to the path of their lockfile.", - ) - register( - "--default-resolve", - type=str, - default="jvm-default", - help=( - "The default value used for the `resolve` field.\n\n" - "The name must be defined as a resolve in `[jvm].resolves`." - ), - ) - register( - "--debug-args", - type=list, - member_type=str, - default=[], - help=( - "Extra JVM arguments to use when running tests in debug mode.\n\n" - "For example, if you want to attach a remote debugger, use something like " - "['-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005']" - ), - ) - - @property - def jdk(self) -> str: - return cast(str, self.options.jdk) - - @property - def tool_jdk(self) -> str: - return cast(str, self.options.tool_jdk) - - @property - def resolves(self) -> dict[str, str]: - return cast("dict[str, str]", dict(self.options.resolves)) - - @property - def default_resolve(self) -> str: - return cast(str, self.options.default_resolve) - - @property - def debug_args(self) -> tuple[str, ...]: - return cast("tuple[str, ...]", tuple(self.options.debug_args)) + tool_jdk = StrOption( + "--tool-jdk", + default="adopt:1.11", + help=( + "The JDK to use when building and running Pants' internal JVM support code and other " + "non-compiler tools. See `jvm` help for supported values." + ), + ).advanced() + jdk = StrOption( + "--jdk", + default="adopt:1.11", + help=( + "The JDK to use.\n\n" + " This string will be passed directly to Coursier's `--jvm` parameter." + " Run `cs java --available` to see a list of available JVM versions on your platform.\n\n" + " If the string 'system' is passed, Coursier's `--system-jvm` option will be used" + " instead, but note that this can lead to inconsistent behavior since the JVM version" + " will be whatever happens to be found first on the system's PATH." + ), + ).advanced() + resolves = DictOption( + "--resolves", + default={"jvm-default": "3rdparty/jvm/default.lock"}, + # TODO: expand help message + help="A dictionary mapping resolve names to the path of their lockfile.", + ) + default_resolve = StrOption( + "--default-resolve", + default="jvm-default", + help=( + "The default value used for the `resolve` and `compatible_resolves` fields.\n\n" + "The name must be defined as a resolve in `[jvm].resolves`." + ), + ) + debug_args = StrListOption( + "--debug-args", + help=( + "Extra JVM arguments to use when running tests in debug mode.\n\n" + "For example, if you want to attach a remote debugger, use something like " + "['-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005']" + ), + ) def resolve_for_target(self, target: Target) -> str | None: """Return the `JvmResolveField` value or its default for the given target. diff --git a/src/python/pants/option/alias.py b/src/python/pants/option/alias.py index e41f6ea7b40..1603188c5fb 100644 --- a/src/python/pants/option/alias.py +++ b/src/python/pants/option/alias.py @@ -11,6 +11,7 @@ from typing import Generator from pants.option.errors import OptionsError +from pants.option.option_types import DictOption from pants.option.scope import ScopeInfo from pants.option.subsystem import Subsystem from pants.util.docutil import bin_name @@ -35,24 +36,20 @@ class CliOptions(Subsystem): options_scope = "cli" help = "Options for configuring CLI behavior, such as command line aliases." - @staticmethod - def register_options(register): - register( - "--alias", - type=dict, - default={}, - help=( - "Register command line aliases.\nExample:\n\n" - " [cli.alias]\n" - ' green = "fmt lint check"\n' - ' all-changed = "--changed-since=HEAD --changed-dependees=transitive"\n' - "\n" - f"This would allow you to run `{bin_name()} green all-changed`, which is shorthand for " - f"`{bin_name()} fmt lint check --changed-since=HEAD --changed-dependees=transitive`.\n\n" - "Notice: this option must be placed in a config file (e.g. `pants.toml` or " - "`pantsrc`) to have any effect." - ), - ) + alias = DictOption[str]( + "--alias", + help=( + "Register command line aliases.\nExample:\n\n" + " [cli.alias]\n" + ' green = "fmt lint check"\n' + ' all-changed = "--changed-since=HEAD --changed-dependees=transitive"\n' + "\n" + f"This would allow you to run `{bin_name()} green all-changed`, which is shorthand for " + f"`{bin_name()} fmt lint check --changed-since=HEAD --changed-dependees=transitive`.\n\n" + "Notice: this option must be placed in a config file (e.g. `pants.toml` or " + "`pantsrc`) to have any effect." + ), + ) @dataclass(frozen=True)