예제 #1
0
class Black(PythonToolBase):
    options_scope = "black"
    name = "Black"
    help = "The Black Python code formatter (https://black.readthedocs.io/)."

    default_version = "black==22.1.0"
    default_main = ConsoleScript("black")

    register_interpreter_constraints = True
    default_interpreter_constraints = ["CPython>=3.7,<4"]

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.black", "black.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/black/black.lock"
    default_lockfile_url = git_url(default_lockfile_path)
    default_extra_requirements = ['typing-extensions>=3.10.0.0; python_version < "3.10"']

    skip = SkipOption("fmt", "lint")
    args = ArgsListOption(example="--target-version=py37 --quiet")
    export = ExportToolOption()
    config = FileOption(
        "--config",
        default=None,
        advanced=True,
        help=lambda cls: softwrap(
            f"""
            Path to a TOML config file understood by Black
            (https://github.com/psf/black#configuration-format).

            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: softwrap(
            f"""
            If true, Pants will include any relevant pyproject.toml config files during runs.

            Use `[{cls.options_scope}].config` instead if your config is in a
            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
        # for how Black discovers config.
        candidates = {os.path.join(d, "pyproject.toml"): b"[tool.black]" for d in ("", *dirs)}
        return ConfigFilesRequest(
            specified=self.config,
            specified_option_name=f"[{self.options_scope}].config",
            discovery=self.config_discovery,
            check_content=candidates,
        )
예제 #2
0
class Autoflake(PythonToolBase):
    options_scope = "autoflake"
    name = "Autoflake"
    help = "The Autoflake Python code formatter (https://github.com/myint/autoflake)."

    default_version = "autoflake==1.4"
    default_main = ConsoleScript("autoflake")

    register_interpreter_constraints = True
    default_interpreter_constraints = ["CPython>=3.7,<4"]

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.autoflake",
                                 "autoflake.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/autoflake/autoflake.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    skip = SkipOption("fmt", "lint")
    args = ArgsListOption(example="--target-version=py37 --quiet")
    export = ExportToolOption()
예제 #3
0
class Bandit(PythonToolBase):
    options_scope = "bandit"
    name = "Bandit"
    help = "A tool for finding security issues in Python code (https://bandit.readthedocs.io)."

    # When upgrading, check if Bandit has started using PEP 517 (a `pyproject.toml` file). If so,
    # remove `setuptools` from `default_extra_requirements`.
    default_version = "bandit>=1.7.0,<1.8"
    default_extra_requirements = [
        "setuptools",
        # GitPython 3.1.20 was yanked because it breaks Python 3.8+, but Poetry's lockfile
        # generation still tries to use it. Upgrade this to the newest version once released or
        # when switching away from Poetry.
        "GitPython==3.1.18",
    ]
    default_main = ConsoleScript("bandit")

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.bandit",
                                 "bandit.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/bandit/bandit.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    skip = SkipOption("lint")
    args = ArgsListOption(example="--skip B101,B308 --confidence")
    export = ExportToolOption()
    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:
        # Refer to https://bandit.readthedocs.io/en/latest/config.html. Note that there are no
        # default locations for Bandit config files.
        return ConfigFilesRequest(
            specified=self.config,
            specified_option_name=f"{self.options_scope}.config")
예제 #4
0
class PyUpgrade(PythonToolBase):
    options_scope = "pyupgrade"
    name = "pyupgrade"
    help = (
        "Upgrade syntax for newer versions of the language (https://github.com/asottile/pyupgrade)."
    )

    default_version = "pyupgrade>=2.31.0,<2.32"
    default_main = ConsoleScript("pyupgrade")

    register_interpreter_constraints = True
    default_interpreter_constraints = ["CPython>=3.7,<4"]

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.pyupgrade",
                                 "pyupgrade.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/pyupgrade/pyupgrade.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    skip = SkipOption("fmt", "lint")
    args = ArgsListOption(example="--py39-plus --keep-runtime-typing")
    export = ExportToolOption()
예제 #5
0
class ClangFormat(PythonToolBase):
    options_scope = "clang-format"
    name = "ClangFormat"
    help = softwrap(
        """
        The clang-format utility for formatting C/C++ (and others) code
        (https://clang.llvm.org/docs/ClangFormat.html). The clang-format binaries
        are retrieved from PyPi (https://pypi.org/project/clang-format/).
        """
    )

    default_version = "clang-format==14.0.3"
    default_main = ConsoleScript("clang-format")

    register_interpreter_constraints = True
    default_interpreter_constraints = ["CPython>=3.7,<4"]

    skip = SkipOption("fmt", "lint")
    args = ArgsListOption(example="--version")

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.cc.lint.clangformat", "clangformat.lock")
    default_lockfile_path = "src/python/pants/backend/cc/lint/clangformat/clangformat.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    export = ExportToolOption()

    def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest:
        """clang-format will use the closest configuration file to the file currently being
        formatted, so add all of them."""
        config_files = (
            ".clang-format",
            "_clang-format",
        )
        check_existence = [os.path.join(d, file) for file in config_files for d in ("", *dirs)]
        return ConfigFilesRequest(
            discovery=True,
            check_existence=check_existence,
        )
예제 #6
0
class Isort(PythonToolBase):
    options_scope = "isort"
    name = "isort"
    help = "The Python import sorter tool (https://pycqa.github.io/isort/)."

    default_version = "isort[pyproject,colors]>=5.9.3,<6.0"
    default_main = ConsoleScript("isort")

    register_interpreter_constraints = True
    default_interpreter_constraints = ["CPython>=3.7,<4"]

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.isort",
                                 "isort.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/isort/isort.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    skip = SkipOption("fmt", "lint")
    args = ArgsListOption(example="--case-sensitive --trailing-comma")
    export = ExportToolOption()
    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: softwrap(f"""
            Path to config file understood by isort
            (https://pycqa.github.io/isort/docs/configuration/config_files/).

            Setting this option will disable `[{cls.options_scope}].config_discovery`. Use
            this option if the config is located in a non-standard location.

            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: softwrap(f"""
            If true, Pants will include any relevant config files during
            runs (`.isort.cfg`, `pyproject.toml`, `setup.cfg`, `tox.ini` and `.editorconfig`).

            Use `[{cls.options_scope}].config` instead if your config is in a
            non-standard location.
            """),
    )

    def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest:
        # Refer to https://pycqa.github.io/isort/docs/configuration/config_files/.
        check_existence = []
        check_content = {}
        for d in ("", *dirs):
            check_existence.append(os.path.join(d, ".isort.cfg"))
            check_content.update({
                os.path.join(d, "pyproject.toml"): b"[tool.isort]",
                os.path.join(d, "setup.cfg"): b"[isort]",
                os.path.join(d, "tox.ini"): b"[isort]",
                os.path.join(d, ".editorconfig"): b"[*.py]",
            })

        return ConfigFilesRequest(
            specified=self.config,
            specified_option_name=f"[{self.options_scope}].config",
            discovery=self.config_discovery,
            check_existence=check_existence,
            check_content=check_content,
        )
예제 #7
0
class PyTest(PythonToolBase):
    options_scope = "pytest"
    name = "Pytest"
    help = "The pytest Python test framework (https://docs.pytest.org/)."

    # This should be compatible with requirements.txt, although it can be more precise.
    # TODO: To fix this, we should allow using a `target_option` referring to a
    #  `python_requirement` to override the version.
    # Pytest 7.1.0 introduced a significant bug that is apparently not fixed as of 7.1.1 (the most
    # recent release at the time of writing). see https://github.com/pantsbuild/pants/issues/14990.
    # TODO: Once this issue is fixed, loosen this to allow the version to float above the bad ones.
    #  E.g., as default_version = "pytest>=7,<8,!=7.1.0,!=7.1.1"
    default_version = "pytest==7.0.1"
    default_extra_requirements = ["pytest-cov>=2.12,!=2.12.1,<3.1"]

    default_main = ConsoleScript("pytest")

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.subsystems",
                                 "pytest.lock")
    default_lockfile_path = "src/python/pants/backend/python/subsystems/pytest.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    args = ArgsListOption(example="-k test_foo --quiet", passthrough=True)
    timeouts_enabled = BoolOption(
        "--timeouts",
        default=True,
        help=softwrap("""
            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=softwrap("""
            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.",
    )
    junit_family = StrOption(
        "--junit-family",
        default="xunit2",
        advanced=True,
        help=softwrap("""
            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=softwrap("""
            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=softwrap("""
            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.
            """),
    )

    export = ExportToolOption()

    @property
    def all_requirements(self) -> tuple[str, ...]:
        return (self.version, *self.extra_requirements)

    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.
        check_existence = []
        check_content = {}
        for d in ("", *dirs):
            check_existence.append(os.path.join(d, "pytest.ini"))
            check_content[os.path.join(
                d, "pyproject.toml")] = b"[tool.pytest.ini_options]"
            check_content[os.path.join(d, "tox.ini")] = b"[pytest]"
            check_content[os.path.join(d, "setup.cfg")] = b"[tool:pytest]"

        return ConfigFilesRequest(
            discovery=self.config_discovery,
            check_existence=check_existence,
            check_content=check_content,
        )

    @memoized_method
    def validate_pytest_cov_included(self) -> None:
        for s in self.extra_requirements:
            try:
                req = PipRequirement.parse(s).project_name
            except Exception as e:
                raise ValueError(
                    f"Invalid requirement '{s}' in `[pytest].extra_requirements`: {e}"
                )
            if canonicalize_project_name(req) == "pytest-cov":
                return

        raise ValueError(
            softwrap(f"""
                You set `[test].use_coverage`, but `[pytest].extra_requirements` is missing
                `pytest-cov`, which is needed to collect coverage data.

                This happens when overriding the `extra_requirements` option. Please either explicitly
                add back `pytest-cov` or use `extra_requirements.add` to keep Pants's default, rather than
                overriding it. Run `{bin_name()} help-advanced pytest` to see the default version of
                `pytest-cov` and see {doc_url('options#list-values')} for more on adding vs.
                overriding list options.
                """))
예제 #8
0
class Pylint(PythonToolBase):
    options_scope = "pylint"
    name = "Pylint"
    help = "The Pylint linter for Python code (https://www.pylint.org/)."

    default_version = "pylint>=2.11.0,<2.12"
    default_main = ConsoleScript("pylint")

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.pylint", "pylint.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/pylint/pylint.lock"
    default_lockfile_url = git_url(default_lockfile_path)
    uses_requirements_from_source_plugins = True

    skip = SkipOption("lint")
    args = ArgsListOption(example="--ignore=foo.py,bar.py --disable=C0330,W0311")
    export = ExportToolOption()
    config = FileOption(
        "--config",
        default=None,
        advanced=True,
        help=lambda cls: softwrap(
            f"""
            Path to a config file understood by Pylint
            (http://pylint.pycqa.org/en/latest/user_guide/run.html#command-line-options).

            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: softwrap(
            f"""
            If true, Pants will include any relevant config files during
            runs (`.pylintrc`, `pylintrc`, `pyproject.toml`, and `setup.cfg`).

            Use `[{cls.options_scope}].config` instead if your config is in a
            non-standard location.
            """
        ),
    )
    _source_plugins = TargetListOption(
        "--source-plugins",
        advanced=True,
        help=softwrap(
            f"""
            An optional list of `python_sources` target addresses to load first-party plugins.

            You 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
            {doc_url('source-roots')}

            You must also set `load-plugins=$module_name` in your Pylint config file.

            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.

            To instead load third-party plugins, set the
            option `[pylint].extra_requirements` and set the `load-plugins` option in your
            Pylint config.

            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
        # how config files are discovered.
        return ConfigFilesRequest(
            specified=self.config,
            specified_option_name=f"[{self.options_scope}].config",
            discovery=self.config_discovery,
            check_existence=[".pylintrc", *(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._source_plugins,
            owning_address=None,
            description_of_origin=f"the option `[{self.options_scope}].source_plugins`",
        )
예제 #9
0
class Yapf(PythonToolBase):
    options_scope = "yapf"
    name = "yapf"
    help = "A formatter for Python files (https://github.com/google/yapf)."

    default_version = "yapf==0.32.0"
    default_extra_requirements = ["toml"]
    default_main = ConsoleScript("yapf")

    register_interpreter_constraints = True
    default_interpreter_constraints = ["CPython>=3.7,<4"]

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.yapf", "yapf.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/yapf/yapf.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    skip = SkipOption("fmt", "lint")
    args = ArgsListOption(
        example="--no-local-style",
        extra_help=softwrap(
            """
            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.
            """
        ),
    )
    export = ExportToolOption()
    config = FileOption(
        "--config",
        default=None,
        advanced=True,
        help=lambda cls: softwrap(
            f"""
            Path to style file understood by yapf
            (https://github.com/google/yapf#formatting-style/).

            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: softwrap(
            f"""
            If true, Pants will include any relevant config files during
            runs (`.style.yapf`, `pyproject.toml`, and `setup.cfg`).

            Use `[{cls.options_scope}].config` instead if your config is in a
            non-standard location.
            """
        ),
    )

    def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest:
        # Refer to https://github.com/google/yapf#formatting-style.
        check_existence = []
        check_content = {}
        for d in ("", *dirs):
            check_existence.append(os.path.join(d, ".yapfignore"))
            check_content.update(
                {
                    os.path.join(d, "pyproject.toml"): b"[tool.yapf",
                    os.path.join(d, "setup.cfg"): b"[yapf]",
                    os.path.join(d, ".style.yapf"): b"[style]",
                }
            )

        return ConfigFilesRequest(
            specified=self.config,
            specified_option_name=f"[{self.options_scope}].config",
            discovery=self.config_discovery,
            check_existence=check_existence,
            check_content=check_content,
        )
예제 #10
0
class Flake8(PythonToolBase):
    options_scope = "flake8"
    name = "Flake8"
    help = "The Flake8 Python linter (https://flake8.pycqa.org/)."

    default_version = "flake8>=3.9.2,<4.0"
    default_main = ConsoleScript("flake8")

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.lint.flake8",
                                 "flake8.lock")
    default_lockfile_path = "src/python/pants/backend/python/lint/flake8/flake8.lock"
    default_lockfile_url = git_url(default_lockfile_path)

    skip = SkipOption("lint")
    args = ArgsListOption(
        example="--ignore E123,W456 --enable-extensions H111")
    export = ExportToolOption()
    config = FileOption(
        "--config",
        default=None,
        advanced=True,
        help=lambda cls: softwrap(f"""
            Path to an INI config file understood by Flake8
            (https://flake8.pycqa.org/en/latest/user/configuration.html).

            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: softwrap(f"""
            If true, Pants will include any relevant config files during
            runs (`.flake8`, `flake8`, `setup.cfg`, and `tox.ini`).

            Use `[{cls.options_scope}].config` instead if your config is in a
            non-standard location.
            """),
    )
    _source_plugins = TargetListOption(
        "--source-plugins",
        advanced=True,
        help=softwrap(f"""
            An optional list of `python_sources` target addresses to load first-party plugins.

            You 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
            {doc_url('source-roots')}

            You must also set `[flake8:local-plugins]` in your Flake8 config file.

            For example:

                ```
                [flake8:local-plugins]
                    extension =
                        CUSTOMCODE = custom_plugin:MyChecker
                ```

            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.

            To instead load third-party plugins, set the option
            `[flake8].extra_requirements`.

            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:
        # See https://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations
        # for how Flake8 discovers config files.
        return ConfigFilesRequest(
            specified=self.config,
            specified_option_name=f"[{self.options_scope}].config",
            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._source_plugins,
            owning_address=None,
            description_of_origin=
            f"the option `[{self.options_scope}].source_plugins`",
        )
예제 #11
0
class MyPy(PythonToolBase):
    options_scope = "mypy"
    name = "MyPy"
    help = "The MyPy Python type checker (http://mypy-lang.org/)."

    default_version = "mypy==0.950"
    default_main = ConsoleScript("mypy")

    # See `mypy/rules.py`. We only use these default constraints in some situations.
    register_interpreter_constraints = True
    default_interpreter_constraints = ["CPython>=3.7,<4"]

    register_lockfile = True
    default_lockfile_resource = ("pants.backend.python.typecheck.mypy",
                                 "mypy.lock")
    default_lockfile_path = "src/python/pants/backend/python/typecheck/mypy/mypy.lock"
    default_lockfile_url = git_url(default_lockfile_path)
    uses_requirements_from_source_plugins = True

    skip = SkipOption("check")
    args = ArgsListOption(example="--python-version 3.7 --disallow-any-expr")
    export = ExportToolOption()
    config = FileOption(
        "--config",
        default=None,
        advanced=True,
        help=lambda cls: softwrap(f"""
            Path to a config file understood by MyPy
            (https://mypy.readthedocs.io/en/stable/config_file.html).

            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: softwrap(f"""
            If true, Pants will include any relevant config files during runs
            (`mypy.ini`, `.mypy.ini`, and `setup.cfg`).

            Use `[{cls.options_scope}].config` instead if your config is in a non-standard location.
            """),
    )
    _source_plugins = TargetListOption(
        "--source-plugins",
        advanced=True,
        help=softwrap("""
            An optional list of `python_sources` target addresses to load first-party plugins.

            You must also set `plugins = path.to.module` in your `mypy.ini`, and
            set the `[mypy].config` option in your `pants.toml`.

            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=softwrap("""
            Extra type stub requirements to install when running MyPy.

            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.

            Expects a list of pip-style requirement strings, like
            `['types-requests==2.25.9']`.
            """),
    )

    @property
    def config_request(self) -> ConfigFilesRequest:
        # Refer to https://mypy.readthedocs.io/en/stable/config_file.html.
        return ConfigFilesRequest(
            specified=self.config,
            specified_option_name=f"{self.options_scope}.config",
            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._source_plugins,
            owning_address=None,
            description_of_origin=
            f"the option `[{self.options_scope}].source_plugins`",
        )

    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."""
        configured = []
        if config and b"python_version" in config.content:
            configured.append(
                softwrap(f"""
                    `python_version` in {config.path} (which is used because of either config
                    discovery or the `[mypy].config` option)
                    """))
        if "--py2" in self.args:
            configured.append("`--py2` in the `--mypy-args` option")
        if any(arg.startswith("--python-version") for arg in self.args):
            configured.append("`--python-version` in the `--mypy-args` option")
        if configured:
            formatted_configured = " and you set ".join(configured)
            logger.warning(
                softwrap(f"""
                    You set {formatted_configured}. Normally, Pants would automatically set this
                    for you based on your code's interpreter constraints
                    ({doc_url('python-interpreter-compatibility')}). Instead, it will
                    use what you set.

                    (Automatically setting the option allows Pants to partition your targets by their
                    constraints, so that, for example, you can run MyPy on Python 2-only code and
                    Python 3-only code at the same time. This feature may no longer work.)
                    """))
        return bool(configured)