python: remove the `Instance` collector node
This commit is contained in:
		
							parent
							
								
									17b38259fd
								
							
						
					
					
						commit
						062d91ab47
					
				|  | @ -0,0 +1,3 @@ | |||
| The ``pytest.Instance`` collector type has been removed. | ||||
| Importing ``pytest.Instance`` or ``_pytest.python.Instance`` returns a dummy type and emits a deprecation warning. | ||||
| See :ref:`instance-collector-deprecation` for details. | ||||
|  | @ -18,6 +18,25 @@ Deprecated Features | |||
| Below is a complete list of all pytest features which are considered deprecated. Using those features will issue | ||||
| :class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`. | ||||
| 
 | ||||
| .. _instance-collector-deprecation: | ||||
| 
 | ||||
| The ``pytest.Instance`` collector | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. versionremoved:: 7.0 | ||||
| 
 | ||||
| The ``pytest.Instance`` collector type has been removed. | ||||
| 
 | ||||
| Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`. | ||||
| Now :class:`~pytest.Class` collects the test methods directly. | ||||
| 
 | ||||
| Most plugins which reference ``Instance`` do so in order to ignore or skip it, | ||||
| using a check such as ``if isinstance(node, Instance): return``. | ||||
| Such plugins should simply remove consideration of ``Instance`` on pytest>=7. | ||||
| However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``, | ||||
| and importing it emits a deprecation warning. This will be removed in pytest 8. | ||||
| 
 | ||||
| 
 | ||||
| .. _node-ctor-fspath-deprecation: | ||||
| 
 | ||||
| ``fspath`` argument for Node constructors replaced with ``pathlib.Path`` | ||||
|  |  | |||
|  | @ -119,6 +119,11 @@ KEYWORD_MSG_ARG = UnformattedWarning( | |||
|     "pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead", | ||||
| ) | ||||
| 
 | ||||
| INSTANCE_COLLECTOR = PytestDeprecationWarning( | ||||
|     "The pytest.Instance collector type is deprecated and is no longer used. " | ||||
|     "See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector", | ||||
| ) | ||||
| 
 | ||||
| # You want to make some `__init__` or function "private". | ||||
| # | ||||
| #   def my_private_function(some, args): | ||||
|  |  | |||
|  | @ -450,10 +450,6 @@ def pytest_unconfigure(config: Config) -> None: | |||
| def mangle_test_address(address: str) -> List[str]: | ||||
|     path, possible_open_bracket, params = address.partition("[") | ||||
|     names = path.split("::") | ||||
|     try: | ||||
|         names.remove("()") | ||||
|     except ValueError: | ||||
|         pass | ||||
|     # Convert file path to dotted path. | ||||
|     names[0] = names[0].replace(nodes.SEP, ".") | ||||
|     names[0] = re.sub(r"\.py$", "", names[0]) | ||||
|  |  | |||
|  | @ -781,9 +781,6 @@ class Session(nodes.FSCollector): | |||
|                                     submatchnodes.append(r) | ||||
|                             if submatchnodes: | ||||
|                                 work.append((submatchnodes, matchnames[1:])) | ||||
|                             # XXX Accept IDs that don't have "()" for class instances. | ||||
|                             elif len(rep.result) == 1 and rep.result[0].name == "()": | ||||
|                                 work.append((rep.result, matchnames)) | ||||
|                         else: | ||||
|                             # Report collection failures here to avoid failing to run some test | ||||
|                             # specified in the command line because the module could not be | ||||
|  |  | |||
|  | @ -158,7 +158,7 @@ class KeywordMatcher: | |||
|         import pytest | ||||
| 
 | ||||
|         for node in item.listchain(): | ||||
|             if not isinstance(node, (pytest.Instance, pytest.Session)): | ||||
|             if not isinstance(node, pytest.Session): | ||||
|                 mapped_names.add(node.name) | ||||
| 
 | ||||
|         # Add the names added as extra keywords to current or parent items. | ||||
|  |  | |||
|  | @ -225,9 +225,7 @@ class Node(metaclass=NodeMeta): | |||
|         else: | ||||
|             if not self.parent: | ||||
|                 raise TypeError("nodeid or parent must be provided") | ||||
|             self._nodeid = self.parent.nodeid | ||||
|             if self.name != "()": | ||||
|                 self._nodeid += "::" + self.name | ||||
|             self._nodeid = self.parent.nodeid + "::" + self.name | ||||
| 
 | ||||
|         #: A place where plugins can store information on the node for their | ||||
|         #: own use. | ||||
|  |  | |||
|  | @ -58,6 +58,7 @@ from _pytest.config import hookimpl | |||
| from _pytest.config.argparsing import Parser | ||||
| from _pytest.deprecated import check_ispytest | ||||
| from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH | ||||
| from _pytest.deprecated import INSTANCE_COLLECTOR | ||||
| from _pytest.fixtures import FuncFixtureInfo | ||||
| from _pytest.main import Session | ||||
| from _pytest.mark import MARK_GEN | ||||
|  | @ -275,12 +276,6 @@ class PyobjMixin(nodes.Node): | |||
|         node = self.getparent(Class) | ||||
|         return node.obj if node is not None else None | ||||
| 
 | ||||
|     @property | ||||
|     def instance(self): | ||||
|         """Python instance object this node was collected from (can be None).""" | ||||
|         node = self.getparent(Instance) | ||||
|         return node.obj if node is not None else None | ||||
| 
 | ||||
|     @property | ||||
|     def obj(self): | ||||
|         """Underlying Python object.""" | ||||
|  | @ -288,7 +283,7 @@ class PyobjMixin(nodes.Node): | |||
|         if obj is None: | ||||
|             self._obj = obj = self._getobj() | ||||
|             # XXX evil hack | ||||
|             # used to avoid Instance collector marker duplication | ||||
|             # used to avoid Function marker duplication | ||||
|             if self._ALLOW_MARKERS: | ||||
|                 self.own_markers.extend(get_unpacked_marks(self.obj)) | ||||
|         return obj | ||||
|  | @ -310,8 +305,6 @@ class PyobjMixin(nodes.Node): | |||
|         chain.reverse() | ||||
|         parts = [] | ||||
|         for node in chain: | ||||
|             if isinstance(node, Instance): | ||||
|                 continue | ||||
|             name = node.name | ||||
|             if isinstance(node, Module): | ||||
|                 name = os.path.splitext(name)[0] | ||||
|  | @ -410,8 +403,9 @@ class PyCollector(PyobjMixin, nodes.Collector): | |||
| 
 | ||||
|         # Avoid random getattrs and peek in the __dict__ instead. | ||||
|         dicts = [getattr(self.obj, "__dict__", {})] | ||||
|         for basecls in self.obj.__class__.__mro__: | ||||
|             dicts.append(basecls.__dict__) | ||||
|         if isinstance(self.obj, type): | ||||
|             for basecls in self.obj.__mro__: | ||||
|                 dicts.append(basecls.__dict__) | ||||
| 
 | ||||
|         # In each class, nodes should be definition ordered. Since Python 3.6, | ||||
|         # __dict__ is definition ordered. | ||||
|  | @ -491,7 +485,6 @@ class PyCollector(PyobjMixin, nodes.Collector): | |||
|                     self, | ||||
|                     name=subname, | ||||
|                     callspec=callspec, | ||||
|                     callobj=funcobj, | ||||
|                     fixtureinfo=fixtureinfo, | ||||
|                     keywords={callspec.id: True}, | ||||
|                     originalname=name, | ||||
|  | @ -776,6 +769,9 @@ class Class(PyCollector): | |||
|         """The public constructor.""" | ||||
|         return super().from_parent(name=name, parent=parent, **kw) | ||||
| 
 | ||||
|     def newinstance(self): | ||||
|         return self.obj() | ||||
| 
 | ||||
|     def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: | ||||
|         if not safe_getattr(self.obj, "__test__", True): | ||||
|             return [] | ||||
|  | @ -803,7 +799,9 @@ class Class(PyCollector): | |||
|         self._inject_setup_class_fixture() | ||||
|         self._inject_setup_method_fixture() | ||||
| 
 | ||||
|         return [Instance.from_parent(self, name="()")] | ||||
|         self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) | ||||
| 
 | ||||
|         return super().collect() | ||||
| 
 | ||||
|     def _inject_setup_class_fixture(self) -> None: | ||||
|         """Inject a hidden autouse, class scoped fixture into the collected class object | ||||
|  | @ -874,25 +872,22 @@ class Class(PyCollector): | |||
|         self.obj.__pytest_setup_method = xunit_setup_method_fixture | ||||
| 
 | ||||
| 
 | ||||
| class Instance(PyCollector): | ||||
|     _ALLOW_MARKERS = False  # hack, destroy later | ||||
|     # Instances share the object with their parents in a way | ||||
|     # that duplicates markers instances if not taken out | ||||
|     # can be removed at node structure reorganization time. | ||||
| class InstanceDummy: | ||||
|     """Instance used to be a node type between Class and Function. It has been | ||||
|     removed in pytest 7.0. Some plugins exist which reference `pytest.Instance` | ||||
|     only to ignore it; this dummy class keeps them working. This will be removed | ||||
|     in pytest 8.""" | ||||
| 
 | ||||
|     def _getobj(self): | ||||
|         # TODO: Improve the type of `parent` such that assert/ignore aren't needed. | ||||
|         assert self.parent is not None | ||||
|         obj = self.parent.obj  # type: ignore[attr-defined] | ||||
|         return obj() | ||||
|     pass | ||||
| 
 | ||||
|     def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: | ||||
|         self.session._fixturemanager.parsefactories(self) | ||||
|         return super().collect() | ||||
| 
 | ||||
|     def newinstance(self): | ||||
|         self.obj = self._getobj() | ||||
|         return self.obj | ||||
| # Note: module __getattr__ only works on Python>=3.7. Unfortunately | ||||
| # we can't provide this deprecation warning on Python 3.6. | ||||
| def __getattr__(name: str) -> object: | ||||
|     if name == "Instance": | ||||
|         warnings.warn(INSTANCE_COLLECTOR, 2) | ||||
|         return InstanceDummy | ||||
|     raise AttributeError(f"module {__name__} has no attribute {name}") | ||||
| 
 | ||||
| 
 | ||||
| def hasinit(obj: object) -> bool: | ||||
|  | @ -1686,9 +1681,23 @@ class Function(PyobjMixin, nodes.Item): | |||
|         """Underlying python 'function' object.""" | ||||
|         return getimfunc(self.obj) | ||||
| 
 | ||||
|     @property | ||||
|     def instance(self): | ||||
|         """Python instance object the function is bound to. | ||||
| 
 | ||||
|         Returns None if not a test method, e.g. for a standalone test function | ||||
|         or a staticmethod. | ||||
|         """ | ||||
|         return getattr(self.obj, "__self__", None) | ||||
| 
 | ||||
|     def _getobj(self): | ||||
|         assert self.parent is not None | ||||
|         return getattr(self.parent.obj, self.originalname)  # type: ignore[attr-defined] | ||||
|         if isinstance(self.parent, Class): | ||||
|             # Each Function gets a fresh class instance. | ||||
|             parent_obj = self.parent.newinstance() | ||||
|         else: | ||||
|             parent_obj = self.parent.obj  # type: ignore[attr-defined] | ||||
|         return getattr(parent_obj, self.originalname) | ||||
| 
 | ||||
|     @property | ||||
|     def _pyfuncitem(self): | ||||
|  | @ -1700,9 +1709,6 @@ class Function(PyobjMixin, nodes.Item): | |||
|         self.ihook.pytest_pyfunc_call(pyfuncitem=self) | ||||
| 
 | ||||
|     def setup(self) -> None: | ||||
|         if isinstance(self.parent, Instance): | ||||
|             self.parent.newinstance() | ||||
|             self.obj = self._getobj() | ||||
|         self._request._fillfixtures() | ||||
| 
 | ||||
|     def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: | ||||
|  |  | |||
|  | @ -756,9 +756,6 @@ class TerminalReporter: | |||
|                     rep.toterminal(self._tw) | ||||
| 
 | ||||
|     def _printcollecteditems(self, items: Sequence[Item]) -> None: | ||||
|         # To print out items and their parent collectors | ||||
|         # we take care to leave out Instances aka () | ||||
|         # because later versions are going to get rid of them anyway. | ||||
|         if self.config.option.verbose < 0: | ||||
|             if self.config.option.verbose < -1: | ||||
|                 counts = Counter(item.nodeid.split("::", 1)[0] for item in items) | ||||
|  | @ -778,8 +775,6 @@ class TerminalReporter: | |||
|                 stack.pop() | ||||
|             for col in needed_collectors[len(stack) :]: | ||||
|                 stack.append(col) | ||||
|                 if col.name == "()":  # Skip Instances. | ||||
|                     continue | ||||
|                 indent = (len(stack) - 1) * "  " | ||||
|                 self._tw.line(f"{indent}{col}") | ||||
|                 if self.config.option.verbose >= 1: | ||||
|  |  | |||
|  | @ -48,7 +48,6 @@ from _pytest.pytester import RecordedHookCall | |||
| from _pytest.pytester import RunResult | ||||
| from _pytest.python import Class | ||||
| from _pytest.python import Function | ||||
| from _pytest.python import Instance | ||||
| from _pytest.python import Metafunc | ||||
| from _pytest.python import Module | ||||
| from _pytest.python import Package | ||||
|  | @ -77,6 +76,7 @@ from _pytest.warning_types import PytestWarning | |||
| 
 | ||||
| set_trace = __pytestPDB.set_trace | ||||
| 
 | ||||
| 
 | ||||
| __all__ = [ | ||||
|     "__version__", | ||||
|     "_fillfuncargs", | ||||
|  | @ -106,7 +106,6 @@ __all__ = [ | |||
|     "HookRecorder", | ||||
|     "hookspec", | ||||
|     "importorskip", | ||||
|     "Instance", | ||||
|     "Item", | ||||
|     "LineMatcher", | ||||
|     "LogCaptureFixture", | ||||
|  | @ -153,3 +152,12 @@ __all__ = [ | |||
|     "xfail", | ||||
|     "yield_fixture", | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| def __getattr__(name: str) -> object: | ||||
|     if name == "Instance": | ||||
|         # The import emits a deprecation warning. | ||||
|         from _pytest.python import Instance | ||||
| 
 | ||||
|         return Instance | ||||
|     raise AttributeError(f"module {__name__} has no attribute {name}") | ||||
|  |  | |||
|  | @ -11,7 +11,6 @@ COLLECT_FAKEMODULE_ATTRIBUTES = [ | |||
|     "Collector", | ||||
|     "Module", | ||||
|     "Function", | ||||
|     "Instance", | ||||
|     "Session", | ||||
|     "Item", | ||||
|     "Class", | ||||
|  |  | |||
|  | @ -286,3 +286,21 @@ def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: | |||
|             parent=mod.parent, | ||||
|             fspath=legacy_path("bla"), | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif( | ||||
|     sys.version_info < (3, 7), | ||||
|     reason="This deprecation can only be emitted on python>=3.7", | ||||
| ) | ||||
| def test_importing_instance_is_deprecated(pytester: Pytester) -> None: | ||||
|     with pytest.warns( | ||||
|         pytest.PytestDeprecationWarning, | ||||
|         match=re.escape("The pytest.Instance collector type is deprecated"), | ||||
|     ): | ||||
|         pytest.Instance | ||||
| 
 | ||||
|     with pytest.warns( | ||||
|         pytest.PytestDeprecationWarning, | ||||
|         match=re.escape("The pytest.Instance collector type is deprecated"), | ||||
|     ): | ||||
|         from _pytest.python import Instance  # noqa: F401 | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ from _pytest.monkeypatch import MonkeyPatch | |||
| from _pytest.nodes import Collector | ||||
| from _pytest.pytester import Pytester | ||||
| from _pytest.python import Class | ||||
| from _pytest.python import Instance | ||||
| from _pytest.python import Function | ||||
| 
 | ||||
| 
 | ||||
| class TestModule: | ||||
|  | @ -585,7 +585,7 @@ class TestFunction: | |||
|                     pass | ||||
|         """ | ||||
|         ) | ||||
|         colitems = modcol.collect()[0].collect()[0].collect() | ||||
|         colitems = modcol.collect()[0].collect() | ||||
|         assert colitems[0].name == "test1[a-c]" | ||||
|         assert colitems[1].name == "test1[b-c]" | ||||
|         assert colitems[2].name == "test2[a-c]" | ||||
|  | @ -1183,19 +1183,26 @@ class TestReportInfo: | |||
|         modcol = pytester.getmodulecol( | ||||
|             """ | ||||
|             # lineno 0 | ||||
|             class TestClass(object): | ||||
|             class TestClass: | ||||
|                 def __getattr__(self, name): | ||||
|                     return "this is not an int" | ||||
| 
 | ||||
|                 def __class_getattr__(cls, name): | ||||
|                     return "this is not an int" | ||||
| 
 | ||||
|                 def intest_foo(self): | ||||
|                     pass | ||||
| 
 | ||||
|                 def test_bar(self): | ||||
|                     pass | ||||
|         """ | ||||
|         ) | ||||
|         classcol = pytester.collect_by_name(modcol, "TestClass") | ||||
|         assert isinstance(classcol, Class) | ||||
|         instance = list(classcol.collect())[0] | ||||
|         assert isinstance(instance, Instance) | ||||
|         path, lineno, msg = instance.reportinfo() | ||||
|         path, lineno, msg = classcol.reportinfo() | ||||
|         func = list(classcol.collect())[0] | ||||
|         assert isinstance(func, Function) | ||||
|         path, lineno, msg = func.reportinfo() | ||||
| 
 | ||||
| 
 | ||||
| def test_customized_python_discovery(pytester: Pytester) -> None: | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ from _pytest import runner | |||
| from _pytest._code import getfslineno | ||||
| from _pytest.fixtures import getfixturemarker | ||||
| from _pytest.pytester import Pytester | ||||
| from _pytest.python import Function | ||||
| 
 | ||||
| 
 | ||||
| class TestOEJSKITSpecials: | ||||
|  | @ -475,3 +476,28 @@ class TestParameterize: | |||
|         ) | ||||
|         res = pytester.runpytest("--collect-only") | ||||
|         res.stdout.fnmatch_lines(["*spam-2*", "*ham-2*"]) | ||||
| 
 | ||||
| 
 | ||||
| def test_function_instance(pytester: Pytester) -> None: | ||||
|     items = pytester.getitems( | ||||
|         """ | ||||
|         def test_func(): pass | ||||
|         class TestIt: | ||||
|             def test_method(self): pass | ||||
|             @classmethod | ||||
|             def test_class(cls): pass | ||||
|             @staticmethod | ||||
|             def test_static(): pass | ||||
|         """ | ||||
|     ) | ||||
|     assert len(items) == 3 | ||||
|     assert isinstance(items[0], Function) | ||||
|     assert items[0].name == "test_func" | ||||
|     assert items[0].instance is None | ||||
|     assert isinstance(items[1], Function) | ||||
|     assert items[1].name == "test_method" | ||||
|     assert items[1].instance is not None | ||||
|     assert items[1].instance.__class__.__name__ == "TestIt" | ||||
|     assert isinstance(items[2], Function) | ||||
|     assert items[2].name == "test_static" | ||||
|     assert items[2].instance is None | ||||
|  |  | |||
|  | @ -979,7 +979,7 @@ class TestLastFailed: | |||
|                 "", | ||||
|                 "<Module pkg1/test_1.py>", | ||||
|                 "  <Class TestFoo>", | ||||
|                 "      <Function test_fail>", | ||||
|                 "    <Function test_fail>", | ||||
|                 "  <Function test_other>", | ||||
|                 "", | ||||
|                 "*= 2/3 tests collected (1 deselected) in *", | ||||
|  |  | |||
|  | @ -74,9 +74,7 @@ class TestCollector: | |||
|         ) | ||||
|         cls = pytester.collect_by_name(modcol, "TestClass") | ||||
|         assert isinstance(cls, pytest.Class) | ||||
|         instance = pytester.collect_by_name(cls, "()") | ||||
|         assert isinstance(instance, pytest.Instance) | ||||
|         fn = pytester.collect_by_name(instance, "test_foo") | ||||
|         fn = pytester.collect_by_name(cls, "test_foo") | ||||
|         assert isinstance(fn, pytest.Function) | ||||
| 
 | ||||
|         module_parent = fn.getparent(pytest.Module) | ||||
|  |  | |||
|  | @ -936,7 +936,7 @@ class TestPython: | |||
| def test_mangle_test_address() -> None: | ||||
|     from _pytest.junitxml import mangle_test_address | ||||
| 
 | ||||
|     address = "::".join(["a/my.py.thing.py", "Class", "()", "method", "[a-1-::]"]) | ||||
|     address = "::".join(["a/my.py.thing.py", "Class", "method", "[a-1-::]"]) | ||||
|     newnames = mangle_test_address(address) | ||||
|     assert newnames == ["a.my.py.thing", "Class", "method", "[a-1-::]"] | ||||
| 
 | ||||
|  |  | |||
|  | @ -272,8 +272,10 @@ def test_nose_setup_ordering(pytester: Pytester) -> None: | |||
|         class TestClass(object): | ||||
|             def setup(self): | ||||
|                 assert visited | ||||
|                 self.visited_cls = True | ||||
|             def test_first(self): | ||||
|                 pass | ||||
|                 assert visited | ||||
|                 assert self.visited_cls | ||||
|         """ | ||||
|     ) | ||||
|     result = pytester.runpytest() | ||||
|  |  | |||
|  | @ -227,7 +227,7 @@ class TestNewSession(SessionTests): | |||
|         started = reprec.getcalls("pytest_collectstart") | ||||
|         finished = reprec.getreports("pytest_collectreport") | ||||
|         assert len(started) == len(finished) | ||||
|         assert len(started) == 8 | ||||
|         assert len(started) == 6 | ||||
|         colfail = [x for x in finished if x.failed] | ||||
|         assert len(colfail) == 1 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue