[RFC] python: remove the Instance collector node

This commit is contained in:
Ran Benita 2021-11-04 22:53:59 +02:00
parent f87df9c52e
commit 9a571060fc
14 changed files with 79 additions and 66 deletions

View File

@ -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])

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -272,12 +272,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."""
@ -285,7 +279,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
@ -307,8 +301,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]
@ -407,8 +399,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.
@ -488,7 +481,6 @@ class PyCollector(PyobjMixin, nodes.Collector):
self,
name=subname,
callspec=callspec,
callobj=funcobj,
fixtureinfo=fixtureinfo,
keywords={callspec.id: True},
originalname=name,
@ -773,6 +765,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 []
@ -800,7 +795,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
@ -871,25 +868,12 @@ 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.
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()
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
# 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 could probably
# be removed at some point.
class Instance:
pass
def hasinit(obj: object) -> bool:
@ -1674,9 +1658,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):
@ -1688,9 +1686,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:

View File

@ -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:

View File

@ -11,7 +11,6 @@ COLLECT_FAKEMODULE_ATTRIBUTES = [
"Collector",
"Module",
"Function",
"Instance",
"Session",
"Item",
"Class",

View File

@ -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:

View File

@ -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

View File

@ -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 *",

View File

@ -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)

View File

@ -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-::]"]

View File

@ -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()

View File

@ -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