From 2ec372df8b987207efc4ad0f33c2f82df5c9e2e5 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 19 Dec 2020 22:19:51 +0200
Subject: [PATCH 1/3] mark: export pytest.Mark for typing purposes

The type cannot be constructed directly, but is exported for use in type
annotations, since it is reachable through existing public API.
---
 changelog/7469.deprecation.rst |  5 +++++
 changelog/7469.feature.rst     | 10 +++++++++
 doc/en/reference.rst           |  4 ++--
 src/_pytest/mark/structures.py | 39 +++++++++++++++++++++++++---------
 src/pytest/__init__.py         |  2 ++
 5 files changed, 48 insertions(+), 12 deletions(-)
 create mode 100644 changelog/7469.deprecation.rst
 create mode 100644 changelog/7469.feature.rst

diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst
new file mode 100644
index 000000000..79419dace
--- /dev/null
+++ b/changelog/7469.deprecation.rst
@@ -0,0 +1,5 @@
+Directly constructing the following classes is now deprecated:
+
+- ``_pytest.mark.structures.Mark``
+
+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
new file mode 100644
index 000000000..f4bc52414
--- /dev/null
+++ b/changelog/7469.feature.rst
@@ -0,0 +1,10 @@
+The types of objects used in pytest's API are now exported so they may be used in type annotations.
+
+The newly-exported types are:
+
+- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
+
+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.
+
+Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
diff --git a/doc/en/reference.rst b/doc/en/reference.rst
index 8aa95ca64..3fc62ee72 100644
--- a/doc/en/reference.rst
+++ b/doc/en/reference.rst
@@ -239,7 +239,7 @@ For example:
     def test_function():
         ...
 
-Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected
+Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
 :class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
 :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
 
@@ -863,7 +863,7 @@ MarkGenerator
 Mark
 ~~~~
 
-.. autoclass:: _pytest.mark.structures.Mark
+.. autoclass:: pytest.Mark()
     :members:
 
 
diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py
index 6c126cf4a..29b958687 100644
--- a/src/_pytest/mark/structures.py
+++ b/src/_pytest/mark/structures.py
@@ -28,6 +28,7 @@ from ..compat import final
 from ..compat import NOTSET
 from ..compat import NotSetType
 from _pytest.config import Config
+from _pytest.deprecated import check_ispytest
 from _pytest.outcomes import fail
 from _pytest.warning_types import PytestUnknownMarkWarning
 
@@ -200,21 +201,38 @@ class ParameterSet(
 
 
 @final
-@attr.s(frozen=True)
+@attr.s(frozen=True, init=False, auto_attribs=True)
 class Mark:
     #: Name of the mark.
-    name = attr.ib(type=str)
+    name: str
     #: Positional arguments of the mark decorator.
-    args = attr.ib(type=Tuple[Any, ...])
+    args: Tuple[Any, ...]
     #: Keyword arguments of the mark decorator.
-    kwargs = attr.ib(type=Mapping[str, Any])
+    kwargs: Mapping[str, Any]
 
     #: Source Mark for ids with parametrize Marks.
-    _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False)
+    _param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
     #: Resolved/generated ids with parametrize Marks.
-    _param_ids_generated = attr.ib(
-        type=Optional[Sequence[str]], default=None, repr=False
-    )
+    _param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
+
+    def __init__(
+        self,
+        name: str,
+        args: Tuple[Any, ...],
+        kwargs: Mapping[str, Any],
+        param_ids_from: Optional["Mark"] = None,
+        param_ids_generated: Optional[Sequence[str]] = None,
+        *,
+        _ispytest: bool = False,
+    ) -> None:
+        """:meta private:"""
+        check_ispytest(_ispytest)
+        # Weirdness to bypass frozen=True.
+        object.__setattr__(self, "name", name)
+        object.__setattr__(self, "args", args)
+        object.__setattr__(self, "kwargs", kwargs)
+        object.__setattr__(self, "_param_ids_from", param_ids_from)
+        object.__setattr__(self, "_param_ids_generated", param_ids_generated)
 
     def _has_param_ids(self) -> bool:
         return "ids" in self.kwargs or len(self.args) >= 4
@@ -243,6 +261,7 @@ class Mark:
             self.args + other.args,
             dict(self.kwargs, **other.kwargs),
             param_ids_from=param_ids_from,
+            _ispytest=True,
         )
 
 
@@ -320,7 +339,7 @@ class MarkDecorator:
 
         :rtype: MarkDecorator
         """
-        mark = Mark(self.name, args, kwargs)
+        mark = Mark(self.name, args, kwargs, _ispytest=True)
         return self.__class__(self.mark.combined_with(mark))
 
     # Type ignored because the overloads overlap with an incompatible
@@ -515,7 +534,7 @@ class MarkGenerator:
                     2,
                 )
 
-        return MarkDecorator(Mark(name, (), {}))
+        return MarkDecorator(Mark(name, (), {}, _ispytest=True))
 
 
 MARK_GEN = MarkGenerator()
diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py
index 70177f950..4b194e0c8 100644
--- a/src/pytest/__init__.py
+++ b/src/pytest/__init__.py
@@ -21,6 +21,7 @@ from _pytest.fixtures import yield_fixture
 from _pytest.freeze_support import freeze_includes
 from _pytest.logging import LogCaptureFixture
 from _pytest.main import Session
+from _pytest.mark import Mark
 from _pytest.mark import MARK_GEN as mark
 from _pytest.mark import param
 from _pytest.monkeypatch import MonkeyPatch
@@ -89,6 +90,7 @@ __all__ = [
     "LogCaptureFixture",
     "main",
     "mark",
+    "Mark",
     "Module",
     "MonkeyPatch",
     "Package",

From 69c302479e3f76450f29e7d2de24254d5eda6492 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sun, 20 Dec 2020 15:11:01 +0200
Subject: [PATCH 2/3] mark: export pytest.MarkDecorator for typing purposes

The type cannot be constructed directly, but is exported for use in type
annotations, since it is reachable through existing public API.
---
 changelog/7469.deprecation.rst |  1 +
 changelog/7469.feature.rst     |  1 +
 doc/en/reference.rst           |  2 +-
 src/_pytest/fixtures.py        |  2 +-
 src/_pytest/mark/structures.py | 40 +++++++++++++++++++---------------
 src/pytest/__init__.py         |  2 ++
 6 files changed, 28 insertions(+), 20 deletions(-)

diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst
index 79419dace..6922b3bbb 100644
--- a/changelog/7469.deprecation.rst
+++ b/changelog/7469.deprecation.rst
@@ -1,5 +1,6 @@
 Directly constructing the following classes is now deprecated:
 
 - ``_pytest.mark.structures.Mark``
+- ``_pytest.mark.structures.MarkDecorator``
 
 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 f4bc52414..66113aa58 100644
--- a/changelog/7469.feature.rst
+++ b/changelog/7469.feature.rst
@@ -3,6 +3,7 @@ The types of objects used in pytest's API are now exported so they may be used i
 The newly-exported types are:
 
 - ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
+- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
 
 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/doc/en/reference.rst b/doc/en/reference.rst
index 3fc62ee72..8bd4111a1 100644
--- a/doc/en/reference.rst
+++ b/doc/en/reference.rst
@@ -849,7 +849,7 @@ Item
 MarkDecorator
 ~~~~~~~~~~~~~
 
-.. autoclass:: _pytest.mark.MarkDecorator
+.. autoclass:: pytest.MarkDecorator()
     :members:
 
 
diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py
index c24ab7069..dbb039bf2 100644
--- a/src/_pytest/fixtures.py
+++ b/src/_pytest/fixtures.py
@@ -551,7 +551,7 @@ class FixtureRequest:
         on all function invocations.
 
         :param marker:
-            A :py:class:`_pytest.mark.MarkDecorator` object created by a call
+            A :class:`pytest.MarkDecorator` object created by a call
             to ``pytest.mark.NAME(...)``.
         """
         self.node.add_marker(marker)
diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py
index 29b958687..8bce33e68 100644
--- a/src/_pytest/mark/structures.py
+++ b/src/_pytest/mark/structures.py
@@ -268,14 +268,14 @@ class Mark:
 # A generic parameter designating an object to which a Mark may
 # be applied -- a test function (callable) or class.
 # Note: a lambda is not allowed, but this can't be represented.
-_Markable = TypeVar("_Markable", bound=Union[Callable[..., object], type])
+Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
 
 
-@attr.s
+@attr.s(init=False, auto_attribs=True)
 class MarkDecorator:
     """A decorator for applying a mark on test functions and classes.
 
-    MarkDecorators are created with ``pytest.mark``::
+    ``MarkDecorators`` are created with ``pytest.mark``::
 
         mark1 = pytest.mark.NAME              # Simple MarkDecorator
         mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
@@ -286,7 +286,7 @@ class MarkDecorator:
         def test_function():
             pass
 
-    When a MarkDecorator is called it does the following:
+    When a ``MarkDecorator`` is called, it does the following:
 
     1. If called with a single class as its only positional argument and no
        additional keyword arguments, it attaches the mark to the class so it
@@ -295,19 +295,24 @@ class MarkDecorator:
     2. If called with a single function as its only positional argument and
        no additional keyword arguments, it attaches the mark to the function,
        containing all the arguments already stored internally in the
-       MarkDecorator.
+       ``MarkDecorator``.
 
-    3. When called in any other case, it returns a new MarkDecorator instance
-       with the original MarkDecorator's content updated with the arguments
-       passed to this call.
+    3. When called in any other case, it returns a new ``MarkDecorator``
+       instance with the original ``MarkDecorator``'s content updated with
+       the arguments passed to this call.
 
-    Note: The rules above prevent MarkDecorators from storing only a single
-    function or class reference as their positional argument with no
+    Note: The rules above prevent a ``MarkDecorator`` from storing only a
+    single function or class reference as its positional argument with no
     additional keyword or positional arguments. You can work around this by
     using `with_args()`.
     """
 
-    mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark))
+    mark: Mark
+
+    def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
+        """:meta private:"""
+        check_ispytest(_ispytest)
+        self.mark = mark
 
     @property
     def name(self) -> str:
@@ -326,6 +331,7 @@ class MarkDecorator:
 
     @property
     def markname(self) -> str:
+        """:meta private:"""
         return self.name  # for backward-compat (2.4.1 had this attr)
 
     def __repr__(self) -> str:
@@ -336,17 +342,15 @@ class MarkDecorator:
 
         Unlike calling the MarkDecorator, with_args() can be used even
         if the sole argument is a callable/class.
-
-        :rtype: MarkDecorator
         """
         mark = Mark(self.name, args, kwargs, _ispytest=True)
-        return self.__class__(self.mark.combined_with(mark))
+        return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
 
     # Type ignored because the overloads overlap with an incompatible
     # return type. Not much we can do about that. Thankfully mypy picks
     # the first match so it works out even if we break the rules.
     @overload
-    def __call__(self, arg: _Markable) -> _Markable:  # type: ignore[misc]
+    def __call__(self, arg: Markable) -> Markable:  # type: ignore[misc]
         pass
 
     @overload
@@ -405,7 +409,7 @@ if TYPE_CHECKING:
 
     class _SkipMarkDecorator(MarkDecorator):
         @overload  # type: ignore[override,misc]
-        def __call__(self, arg: _Markable) -> _Markable:
+        def __call__(self, arg: Markable) -> Markable:
             ...
 
         @overload
@@ -423,7 +427,7 @@ if TYPE_CHECKING:
 
     class _XfailMarkDecorator(MarkDecorator):
         @overload  # type: ignore[override,misc]
-        def __call__(self, arg: _Markable) -> _Markable:
+        def __call__(self, arg: Markable) -> Markable:
             ...
 
         @overload
@@ -534,7 +538,7 @@ class MarkGenerator:
                     2,
                 )
 
-        return MarkDecorator(Mark(name, (), {}, _ispytest=True))
+        return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
 
 
 MARK_GEN = MarkGenerator()
diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py
index 4b194e0c8..1d5b38ee0 100644
--- a/src/pytest/__init__.py
+++ b/src/pytest/__init__.py
@@ -23,6 +23,7 @@ from _pytest.logging import LogCaptureFixture
 from _pytest.main import Session
 from _pytest.mark import Mark
 from _pytest.mark import MARK_GEN as mark
+from _pytest.mark import MarkDecorator
 from _pytest.mark import param
 from _pytest.monkeypatch import MonkeyPatch
 from _pytest.nodes import Collector
@@ -91,6 +92,7 @@ __all__ = [
     "main",
     "mark",
     "Mark",
+    "MarkDecorator",
     "Module",
     "MonkeyPatch",
     "Package",

From 6aa4d1c7ab968aecf44ad89e568a4515bd7e5343 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sun, 20 Dec 2020 15:36:24 +0200
Subject: [PATCH 3/3] mark: export pytest.MarkGenerator for typing purposes

The type cannot be constructed directly, but is exported for use in type
annotations, since it is reachable through existing public API.
---
 changelog/7469.deprecation.rst |  1 +
 changelog/7469.feature.rst     |  1 +
 doc/en/reference.rst           |  2 +-
 src/_pytest/mark/structures.py | 11 +++++++----
 src/pytest/__init__.py         |  2 ++
 testing/test_mark.py           |  4 ++--
 6 files changed, 14 insertions(+), 7 deletions(-)

diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst
index 6922b3bbb..6bbc80755 100644
--- a/changelog/7469.deprecation.rst
+++ b/changelog/7469.deprecation.rst
@@ -2,5 +2,6 @@ Directly constructing the following classes is now deprecated:
 
 - ``_pytest.mark.structures.Mark``
 - ``_pytest.mark.structures.MarkDecorator``
+- ``_pytest.mark.structures.MarkGenerator``
 
 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 66113aa58..81f93d1f7 100644
--- a/changelog/7469.feature.rst
+++ b/changelog/7469.feature.rst
@@ -4,6 +4,7 @@ The newly-exported types are:
 
 - ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
 - ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
+- ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
 
 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/doc/en/reference.rst b/doc/en/reference.rst
index 8bd4111a1..c8e8dca75 100644
--- a/doc/en/reference.rst
+++ b/doc/en/reference.rst
@@ -856,7 +856,7 @@ MarkDecorator
 MarkGenerator
 ~~~~~~~~~~~~~
 
-.. autoclass:: _pytest.mark.MarkGenerator
+.. autoclass:: pytest.MarkGenerator()
     :members:
 
 
diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py
index 8bce33e68..ae6920735 100644
--- a/src/_pytest/mark/structures.py
+++ b/src/_pytest/mark/structures.py
@@ -488,9 +488,6 @@ class MarkGenerator:
     applies a 'slowtest' :class:`Mark` on ``test_function``.
     """
 
-    _config: Optional[Config] = None
-    _markers: Set[str] = set()
-
     # See TYPE_CHECKING above.
     if TYPE_CHECKING:
         skip: _SkipMarkDecorator
@@ -500,7 +497,13 @@ class MarkGenerator:
         usefixtures: _UsefixturesMarkDecorator
         filterwarnings: _FilterwarningsMarkDecorator
 
+    def __init__(self, *, _ispytest: bool = False) -> None:
+        check_ispytest(_ispytest)
+        self._config: Optional[Config] = None
+        self._markers: Set[str] = set()
+
     def __getattr__(self, name: str) -> MarkDecorator:
+        """Generate a new :class:`MarkDecorator` with the given name."""
         if name[0] == "_":
             raise AttributeError("Marker name must NOT start with underscore")
 
@@ -541,7 +544,7 @@ class MarkGenerator:
         return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
 
 
-MARK_GEN = MarkGenerator()
+MARK_GEN = MarkGenerator(_ispytest=True)
 
 
 @final
diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py
index 1d5b38ee0..74cf00ee2 100644
--- a/src/pytest/__init__.py
+++ b/src/pytest/__init__.py
@@ -24,6 +24,7 @@ from _pytest.main import Session
 from _pytest.mark import Mark
 from _pytest.mark import MARK_GEN as mark
 from _pytest.mark import MarkDecorator
+from _pytest.mark import MarkGenerator
 from _pytest.mark import param
 from _pytest.monkeypatch import MonkeyPatch
 from _pytest.nodes import Collector
@@ -93,6 +94,7 @@ __all__ = [
     "mark",
     "Mark",
     "MarkDecorator",
+    "MarkGenerator",
     "Module",
     "MonkeyPatch",
     "Package",
diff --git a/testing/test_mark.py b/testing/test_mark.py
index e0b91f0ce..5f4b3e063 100644
--- a/testing/test_mark.py
+++ b/testing/test_mark.py
@@ -21,7 +21,7 @@ class TestMark:
         assert attr in module.__all__  # type: ignore
 
     def test_pytest_mark_notcallable(self) -> None:
-        mark = MarkGenerator()
+        mark = MarkGenerator(_ispytest=True)
         with pytest.raises(TypeError):
             mark()  # type: ignore[operator]
 
@@ -40,7 +40,7 @@ class TestMark:
         assert pytest.mark.foo.with_args(SomeClass) is not SomeClass  # type: ignore[comparison-overlap]
 
     def test_pytest_mark_name_starts_with_underscore(self) -> None:
-        mark = MarkGenerator()
+        mark = MarkGenerator(_ispytest=True)
         with pytest.raises(AttributeError):
             mark._some_name