From 538b5c24999e9ebb4fab43faabc8bcc28737bcdf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 23 May 2021 23:45:49 +0300 Subject: [PATCH] argparsing: export Parser and OptionGroup for typing purposes `Parser` is used by many plugins and custom hooks. `OptionGroup` is exposed by the `parser.addgroup` API. The constructors of both are marked private, they are not meant to be constructed directly. --- changelog/7259.feature.rst | 2 +- changelog/7469.deprecation.rst | 2 ++ changelog/7469.feature.rst | 2 ++ changelog/8315.deprecation.rst | 2 +- doc/en/deprecations.rst | 2 +- doc/en/reference/reference.rst | 7 ++++++- src/_pytest/config/__init__.py | 5 +++-- src/_pytest/config/argparsing.py | 30 +++++++++++++++++++++--------- src/_pytest/hookspec.py | 8 ++++---- src/pytest/__init__.py | 4 ++++ testing/test_parseopt.py | 10 +++++----- 11 files changed, 50 insertions(+), 24 deletions(-) diff --git a/changelog/7259.feature.rst b/changelog/7259.feature.rst index 41a213f63..dd03b4896 100644 --- a/changelog/7259.feature.rst +++ b/changelog/7259.feature.rst @@ -1,7 +1,7 @@ Added :meth:`cache.mkdir() `, which is similar to the existing :meth:`cache.makedir() `, but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``. -Added a ``paths`` type to :meth:`parser.addini() <_pytest.config.argparsing.Parser.addini>`, +Added a ``paths`` type to :meth:`parser.addini() `, as in ``parser.addini("mypaths", "my paths", type="paths")``, which is similar to the existing ``pathlist``, but returns a list of :class:`pathlib.Path` instead of legacy ``py.path.local``. diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index e01764caa..3762306c7 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -6,5 +6,7 @@ Directly constructing the following classes is now deprecated: - ``_pytest.python.Metafunc`` - ``_pytest.runner.CallInfo`` - ``_pytest._code.ExceptionInfo`` +- ``_pytest.config.argparsing.Parser`` +- ``_pytest.config.argparsing.OptionGroup`` These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst index ea8df5239..0feef2d7d 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -8,6 +8,8 @@ The newly-exported types are: - ``pytest.Metafunc`` for the :class:`metafunc ` argument to the :func:`pytest_generate_tests ` hook. - ``pytest.CallInfo`` for the :class:`CallInfo ` type passed to various hooks. - ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo ` type returned from :func:`pytest.raises` and passed to various hooks. +- ``pytest.Parser`` for the :class:`Parser ` type passed to the :func:`pytest_addoption ` hook. +- ``pytest.OptionGroup`` for the :class:`OptionGroup ` type returned from the :func:`parser.addgroup ` method. Constructing them directly is not supported; they are only meant for use in type annotations. Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. diff --git a/changelog/8315.deprecation.rst b/changelog/8315.deprecation.rst index 9b49d7c2f..c30dcf5d1 100644 --- a/changelog/8315.deprecation.rst +++ b/changelog/8315.deprecation.rst @@ -1,4 +1,4 @@ -Several behaviors of :meth:`Parser.addoption <_pytest.config.argparsing.Parser.addoption>` are now +Several behaviors of :meth:`Parser.addoption ` are now scheduled for removal in pytest 7 (deprecated since pytest 2.4.0): - ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index db04a48b3..c3020fb94 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -48,7 +48,7 @@ Backward compatibilities in ``Parser.addoption`` .. deprecated:: 2.4 -Several behaviors of :meth:`Parser.addoption <_pytest.config.argparsing.Parser.addoption>` are now +Several behaviors of :meth:`Parser.addoption ` are now scheduled for removal in pytest 7 (deprecated since pytest 2.4.0): - ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index d4256bd60..95bf6cf62 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -889,9 +889,14 @@ Node Parser ~~~~~~ -.. autoclass:: _pytest.config.argparsing.Parser() +.. autoclass:: pytest.Parser() :members: +OptionGroup +~~~~~~~~~~~ + +.. autoclass:: pytest.OptionGroup() + :members: PytestPluginManager ~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 7f18f62cb..82bc37873 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -909,6 +909,7 @@ class Config: self._parser = Parser( usage=f"%(prog)s [options] [{_a}] [{_a}] [...]", processopt=self._processopt, + _ispytest=True, ) self.pluginmanager = pluginmanager """The plugin manager handles plugin registration and hook invocation. @@ -1380,8 +1381,8 @@ class Config: """Return configuration value from an :ref:`ini file `. If the specified name hasn't been registered through a prior - :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` - call (usually from a plugin), a ValueError is raised. + :func:`parser.addini ` call (usually from a + plugin), a ValueError is raised. """ try: return self._inicache[name] diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d24696eaf..51acb4033 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -21,6 +21,7 @@ from _pytest.config.exceptions import UsageError from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT from _pytest.deprecated import ARGUMENT_TYPE_STR from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE +from _pytest.deprecated import check_ispytest if TYPE_CHECKING: from typing import NoReturn @@ -43,8 +44,11 @@ class Parser: self, usage: Optional[str] = None, processopt: Optional[Callable[["Argument"], None]] = None, + *, + _ispytest: bool = False, ) -> None: - self._anonymous = OptionGroup("custom options", parser=self) + check_ispytest(_ispytest) + self._anonymous = OptionGroup("custom options", parser=self, _ispytest=True) self._groups: List[OptionGroup] = [] self._processopt = processopt self._usage = usage @@ -67,14 +71,14 @@ class Parser: :after: Name of another group, used for ordering --help output. The returned group object has an ``addoption`` method with the same - signature as :py:func:`parser.addoption - <_pytest.config.argparsing.Parser.addoption>` but will be shown in the - respective group in the output of ``pytest. --help``. + signature as :func:`parser.addoption ` but + will be shown in the respective group in the output of + ``pytest. --help``. """ for group in self._groups: if group.name == name: return group - group = OptionGroup(name, description, parser=self) + group = OptionGroup(name, description, parser=self, _ispytest=True) i = 0 for i, grp in enumerate(self._groups): if grp.name == after: @@ -334,9 +338,17 @@ class Argument: class OptionGroup: + """A group of options shown in its own section.""" + def __init__( - self, name: str, description: str = "", parser: Optional[Parser] = None + self, + name: str, + description: str = "", + parser: Optional[Parser] = None, + *, + _ispytest: bool = False, ) -> None: + check_ispytest(_ispytest) self.name = name self.description = description self.options: List[Argument] = [] @@ -346,9 +358,9 @@ class OptionGroup: """Add an option to this group. If a shortened version of a long option is specified, it will - be suppressed in the help. addoption('--twowords', '--two-words') - results in help showing '--two-words' only, but --twowords gets - accepted **and** the automatic destination is in args.twowords. + be suppressed in the help. ``addoption('--twowords', '--two-words')`` + results in help showing ``--two-words`` only, but ``--twowords`` gets + accepted **and** the automatic destination is in ``args.twowords``. """ conflict = set(optnames).intersection( name for opt in self.options for name in opt.names() diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 427d7507e..75110a9a3 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -88,11 +88,11 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. - :param _pytest.config.argparsing.Parser parser: + :param pytest.Parser parser: To add command line options, call - :py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`. + :py:func:`parser.addoption(...) `. To add ini-file values call :py:func:`parser.addini(...) - <_pytest.config.argparsing.Parser.addini>`. + `. :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager, which can be used to install :py:func:`hookspec`'s @@ -193,7 +193,7 @@ def pytest_load_initial_conftests( :param _pytest.config.Config early_config: The pytest config object. :param List[str] args: Arguments passed on the command line. - :param _pytest.config.argparsing.Parser parser: To add command line options. + :param pytest.Parser parser: To add command line options. """ diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 02a82386d..7a81d2341 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -13,6 +13,8 @@ from _pytest.config import hookimpl from _pytest.config import hookspec from _pytest.config import main from _pytest.config import UsageError +from _pytest.config.argparsing import OptionGroup +from _pytest.config.argparsing import Parser from _pytest.debugging import pytestPDB as __pytestPDB from _pytest.fixtures import _fillfuncargs from _pytest.fixtures import fixture @@ -103,8 +105,10 @@ __all__ = [ "Metafunc", "Module", "MonkeyPatch", + "OptionGroup", "Package", "param", + "Parser", "PytestAssertRewriteWarning", "PytestCacheWarning", "PytestCollectionWarning", diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 1062c8c37..28529d043 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -14,12 +14,12 @@ from _pytest.pytester import Pytester @pytest.fixture def parser() -> parseopt.Parser: - return parseopt.Parser() + return parseopt.Parser(_ispytest=True) class TestParser: def test_no_help_by_default(self) -> None: - parser = parseopt.Parser(usage="xyz") + parser = parseopt.Parser(usage="xyz", _ispytest=True) pytest.raises(UsageError, lambda: parser.parse(["-h"])) def test_custom_prog(self, parser: parseopt.Parser) -> None: @@ -90,13 +90,13 @@ class TestParser: assert groups_names == list("132") def test_group_addoption(self) -> None: - group = parseopt.OptionGroup("hello") + group = parseopt.OptionGroup("hello", _ispytest=True) group.addoption("--option1", action="store_true") assert len(group.options) == 1 assert isinstance(group.options[0], parseopt.Argument) def test_group_addoption_conflict(self) -> None: - group = parseopt.OptionGroup("hello again") + group = parseopt.OptionGroup("hello again", _ispytest=True) group.addoption("--option1", "--option-1", action="store_true") with pytest.raises(ValueError) as err: group.addoption("--option1", "--option-one", action="store_true") @@ -188,7 +188,7 @@ class TestParser: elif option.type is str: option.default = "world" - parser = parseopt.Parser(processopt=defaultget) + parser = parseopt.Parser(processopt=defaultget, _ispytest=True) parser.addoption("--this", dest="this", type=int, action="store") parser.addoption("--hello", dest="hello", type=str, action="store") parser.addoption("--no", dest="no", action="store_true")