move python nodes to own module

This commit is contained in:
Ronny Pfannschmidt 2023-05-13 21:57:02 +02:00
parent e1596f44a2
commit 9d26781cb6
18 changed files with 963 additions and 935 deletions

View File

@ -1,14 +1,14 @@
import os.path import os.path
import pytest import _pytest.python.nodes
mydir = os.path.dirname(__file__) mydir = os.path.dirname(__file__)
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
if isinstance(item, pytest.Function): if isinstance(item, _pytest.python.nodes.Function):
if not item.fspath.relto(mydir): if not item.fspath.relto(mydir):
return return
mod = item.getparent(pytest.Module).obj mod = item.getparent(_pytest.python.nodes.Module).obj
if hasattr(mod, "hello"): if hasattr(mod, "hello"):
print(f"mod.hello {mod.hello!r}") print(f"mod.hello {mod.hello!r}")

View File

@ -27,8 +27,8 @@ from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.main import Session from _pytest.main import Session
from _pytest.python import Module from _pytest.python.nodes import Module
from _pytest.python import Package from _pytest.python.nodes import Package
from _pytest.reports import TestReport from _pytest.reports import TestReport
README_CONTENT = """\ README_CONTENT = """\

View File

@ -39,7 +39,7 @@ from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path from _pytest.pathlib import import_path
from _pytest.python import Module from _pytest.python.nodes import Module
from _pytest.python_api import approx from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning

View File

@ -29,7 +29,7 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import _pytest import _pytest.python.nodes
from _pytest import nodes from _pytest import nodes
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import FormattedExcinfo from _pytest._code.code import FormattedExcinfo
@ -111,9 +111,9 @@ def pytest_sessionstart(session: "Session") -> None:
def get_scope_package(node, fixturedef: "FixtureDef[object]"): def get_scope_package(node, fixturedef: "FixtureDef[object]"):
import pytest pass
cls = pytest.Package cls = _pytest.python.nodes.Package
current = node current = node
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
while current and ( while current and (
@ -133,11 +133,11 @@ def get_scope_node(
if scope is Scope.Function: if scope is Scope.Function:
return node.getparent(nodes.Item) return node.getparent(nodes.Item)
elif scope is Scope.Class: elif scope is Scope.Class:
return node.getparent(_pytest.python.Class) return node.getparent(_pytest.python.nodes.Class)
elif scope is Scope.Module: elif scope is Scope.Module:
return node.getparent(_pytest.python.Module) return node.getparent(_pytest.python.nodes.Module)
elif scope is Scope.Package: elif scope is Scope.Package:
return node.getparent(_pytest.python.Package) return node.getparent(_pytest.python.nodes.Package)
elif scope is Scope.Session: elif scope is Scope.Session:
return node.getparent(_pytest.main.Session) return node.getparent(_pytest.main.Session)
else: else:
@ -189,7 +189,7 @@ def add_funcarg_pseudo_fixture_def(
node = get_scope_node(collector, scope) node = get_scope_node(collector, scope)
if node is None: if node is None:
assert scope is Scope.Class and isinstance( assert scope is Scope.Class and isinstance(
collector, _pytest.python.Module collector, _pytest.python.nodes.Module
) )
# Use module-level collector for class-scope (for now). # Use module-level collector for class-scope (for now).
node = collector node = collector
@ -472,7 +472,7 @@ class FixtureRequest:
"""Class (can be None) where the test function was collected.""" """Class (can be None) where the test function was collected."""
if self.scope not in ("class", "function"): if self.scope not in ("class", "function"):
raise AttributeError(f"cls not available in {self.scope}-scoped context") raise AttributeError(f"cls not available in {self.scope}-scoped context")
clscol = self._pyfuncitem.getparent(_pytest.python.Class) clscol = self._pyfuncitem.getparent(_pytest.python.nodes.Class)
if clscol: if clscol:
return clscol.obj return clscol.obj
@ -491,7 +491,7 @@ class FixtureRequest:
"""Python module object where the test function was collected.""" """Python module object where the test function was collected."""
if self.scope not in ("function", "class", "module"): if self.scope not in ("function", "class", "module"):
raise AttributeError(f"module not available in {self.scope}-scoped context") raise AttributeError(f"module not available in {self.scope}-scoped context")
return self._pyfuncitem.getparent(_pytest.python.Module).obj return self._pyfuncitem.getparent(_pytest.python.nodes.Module).obj
@property @property
def path(self) -> Path: def path(self) -> Path:

View File

@ -33,10 +33,8 @@ if TYPE_CHECKING:
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.outcomes import Exit from _pytest.outcomes import Exit
from _pytest.python import Class from _pytest.python.nodes import Class, Module, Function
from _pytest.python.metafunc import Metafunc from _pytest.python.metafunc import Metafunc
from _pytest.python import Function
from _pytest.python import Module
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
from _pytest.reports import TestReport from _pytest.reports import TestReport
from _pytest.runner import CallInfo from _pytest.runner import CallInfo

View File

@ -675,7 +675,7 @@ class Session(nodes.FSCollector):
return items return items
def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
from _pytest.python import Package from _pytest.python.nodes import Package
# Keep track of any collected nodes in here, so we don't duplicate fixtures. # Keep track of any collected nodes in here, so we don't duplicate fixtures.
node_cache1: Dict[Path, Sequence[nodes.Collector]] = {} node_cache1: Dict[Path, Sequence[nodes.Collector]] = {}

View File

@ -5,7 +5,7 @@ from _pytest.config import hookimpl
from _pytest.deprecated import NOSE_SUPPORT from _pytest.deprecated import NOSE_SUPPORT
from _pytest.fixtures import getfixturemarker from _pytest.fixtures import getfixturemarker
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.python import Function from _pytest.python.nodes import Function
from _pytest.unittest import TestCaseFunction from _pytest.unittest import TestCaseFunction

View File

@ -1,60 +1,28 @@
"""Python test discovery, setup and run of test functions.""" """Python test discovery, setup and run of test functions."""
import fnmatch
import inspect import inspect
import os
import sys
import types
import warnings import warnings
from functools import partial
from pathlib import Path from pathlib import Path
from typing import Any
from typing import Dict
from typing import Generator
from typing import Iterable
from typing import Iterator
from typing import List from typing import List
from typing import Mapping
from typing import Optional from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import _pytest from . import nodes
from _pytest import fixtures from .. import nodes as base_nodes
from _pytest import nodes
from _pytest._code import filter_traceback
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.compat import is_async_function from _pytest.compat import is_async_function
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.compat import safe_isclass from _pytest.compat import safe_isclass
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo
from _pytest.main import Session
from _pytest.mark import MARK_GEN from _pytest.mark import MARK_GEN
from _pytest.mark.structures import get_unpacked_marks
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import parts
from _pytest.pathlib import visit
from _pytest.python.metafunc import CallSpec2
from _pytest.python.metafunc import Metafunc from _pytest.python.metafunc import Metafunc
from _pytest.python.nodes import path_matches_patterns
from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestCollectionWarning
from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestReturnNotNoneWarning
from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning
@ -161,7 +129,7 @@ def async_warn_and_skip(nodeid: str) -> None:
@hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_pyfunc_call(pyfuncitem: "nodes.Function") -> Optional[object]:
testfunction = pyfuncitem.obj testfunction = pyfuncitem.obj
if is_async_function(testfunction): if is_async_function(testfunction):
async_warn_and_skip(pyfuncitem.nodeid) async_warn_and_skip(pyfuncitem.nodeid)
@ -180,7 +148,9 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
return True return True
def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]: def pytest_collect_file(
file_path: Path, parent: base_nodes.Collector
) -> Optional["nodes.Module"]:
if file_path.suffix == ".py": if file_path.suffix == ".py":
if not parent.session.isinitpath(file_path): if not parent.session.isinitpath(file_path):
if not path_matches_patterns( if not path_matches_patterns(
@ -188,35 +158,35 @@ def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["M
): ):
return None return None
ihook = parent.session.gethookproxy(file_path) ihook = parent.session.gethookproxy(file_path)
module: Module = ihook.pytest_pycollect_makemodule( module: nodes.Module = ihook.pytest_pycollect_makemodule(
module_path=file_path, parent=parent module_path=file_path, parent=parent
) )
return module return module
return None return None
def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: def pytest_pycollect_makemodule(module_path: Path, parent) -> "nodes.Module":
"""Return whether path matches any of the patterns in the list of globs given."""
return any(fnmatch_ex(pattern, path) for pattern in patterns)
def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
if module_path.name == "__init__.py": if module_path.name == "__init__.py":
pkg: Package = Package.from_parent(parent, path=module_path) pkg: nodes.Package = nodes.Package.from_parent(parent, path=module_path)
return pkg return pkg
mod: Module = Module.from_parent(parent, path=module_path) mod: nodes.Module = nodes.Module.from_parent(parent, path=module_path)
return mod return mod
@hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_pycollect_makeitem( def pytest_pycollect_makeitem(
collector: Union["Module", "Class"], name: str, obj: object collector: Union["nodes.Module", "nodes.Class"], name: str, obj: object
) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]: ) -> Union[
assert isinstance(collector, (Class, Module)), type(collector) None,
base_nodes.Item,
base_nodes.Collector,
List[Union[base_nodes.Item, base_nodes.Collector]],
]:
assert isinstance(collector, (nodes.Class, nodes.Module)), type(collector)
# Nothing was collected elsewhere, let's do it here. # Nothing was collected elsewhere, let's do it here.
if safe_isclass(obj): if safe_isclass(obj):
if collector.istestclass(obj, name): if collector.istestclass(obj, name):
klass: Class = Class.from_parent(collector, name=name, obj=obj) klass: nodes.Class = nodes.Class.from_parent(collector, name=name, obj=obj)
return klass return klass
elif collector.istestfunction(obj, name): elif collector.istestfunction(obj, name):
# mock seems to store unbound methods (issue473), normalize it. # mock seems to store unbound methods (issue473), normalize it.
@ -236,7 +206,7 @@ def pytest_pycollect_makeitem(
) )
elif getattr(obj, "__test__", True): elif getattr(obj, "__test__", True):
if is_generator(obj): if is_generator(obj):
res: Function = Function.from_parent(collector, name=name) res: nodes.Function = nodes.Function.from_parent(collector, name=name)
reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
name=name name=name
) )
@ -248,661 +218,6 @@ def pytest_pycollect_makeitem(
return None return None
class PyobjMixin(nodes.Node):
"""this mix-in inherits from Node to carry over the typing information
as its intended to always mix in before a node
its position in the mro is unaffected"""
_ALLOW_MARKERS = True
@property
def module(self):
"""Python module object this node was collected from (can be None)."""
node = self.getparent(Module)
return node.obj if node is not None else None
@property
def cls(self):
"""Python class object this node was collected from (can be None)."""
node = self.getparent(Class)
return node.obj if node is not None else None
@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,
a staticmethod, a class or a module.
"""
node = self.getparent(Function)
return getattr(node.obj, "__self__", None) if node is not None else None
@property
def obj(self):
"""Underlying Python object."""
obj = getattr(self, "_obj", None)
if obj is None:
self._obj = obj = self._getobj()
# XXX evil hack
# used to avoid Function marker duplication
if self._ALLOW_MARKERS:
self.own_markers.extend(get_unpacked_marks(self.obj))
# This assumes that `obj` is called before there is a chance
# to add custom keys to `self.keywords`, so no fear of overriding.
self.keywords.update((mark.name, mark) for mark in self.own_markers)
return obj
@obj.setter
def obj(self, value):
self._obj = value
def _getobj(self):
"""Get the underlying Python object. May be overwritten by subclasses."""
# 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 getattr(obj, self.name)
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
"""Return Python path relative to the containing module."""
chain = self.listchain()
chain.reverse()
parts = []
for node in chain:
name = node.name
if isinstance(node, Module):
name = os.path.splitext(name)[0]
if stopatmodule:
if includemodule:
parts.append(name)
break
parts.append(name)
parts.reverse()
return ".".join(parts)
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
# XXX caching?
obj = self.obj
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
if isinstance(compat_co_firstlineno, int):
# nose compatibility
file_path = sys.modules[obj.__module__].__file__
assert file_path is not None
if file_path.endswith(".pyc"):
file_path = file_path[:-1]
path: Union["os.PathLike[str]", str] = file_path
lineno = compat_co_firstlineno
else:
path, lineno = getfslineno(obj)
modpath = self.getmodpath()
assert isinstance(lineno, int)
return path, lineno, modpath
# As an optimization, these builtin attribute names are pre-ignored when
# iterating over an object during collection -- the pytest_pycollect_makeitem
# hook is not called for them.
# fmt: off
class _EmptyClass: pass # noqa: E701
IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
frozenset(),
# Module.
dir(types.ModuleType("empty_module")),
# Some extra module attributes the above doesn't catch.
{"__builtins__", "__file__", "__cached__"},
# Class.
dir(_EmptyClass),
# Instance.
dir(_EmptyClass()),
)
del _EmptyClass
# fmt: on
class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name)
def isnosetest(self, obj: object) -> bool:
"""Look for the __test__ attribute, which is applied by the
@nose.tools.istest decorator.
"""
# We explicitly check for "is True" here to not mistakenly treat
# classes with a custom __getattr__ returning something truthy (like a
# function) as test classes.
return safe_getattr(obj, "__test__", False) is True
def classnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_classes", name)
def istestfunction(self, obj: object, name: str) -> bool:
if self.funcnamefilter(name) or self.isnosetest(obj):
if isinstance(obj, (staticmethod, classmethod)):
# staticmethods and classmethods need to be unwrapped.
obj = safe_getattr(obj, "__func__", False)
return callable(obj) and fixtures.getfixturemarker(obj) is None
else:
return False
def istestclass(self, obj: object, name: str) -> bool:
return self.classnamefilter(name) or self.isnosetest(obj)
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
"""Check if the given name matches the prefix or glob-pattern defined
in ini configuration."""
for option in self.config.getini(option_name):
if name.startswith(option):
return True
# Check that name looks like a glob-string before calling fnmatch
# because this is called for every name in each collected module,
# and fnmatch is somewhat expensive to call.
elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
name, option
):
return True
return False
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
if not getattr(self.obj, "__test__", True):
return []
# Avoid random getattrs and peek in the __dict__ instead.
dicts = [getattr(self.obj, "__dict__", {})]
if isinstance(self.obj, type):
for basecls in self.obj.__mro__:
dicts.append(basecls.__dict__)
# In each class, nodes should be definition ordered.
# __dict__ is definition ordered.
seen: Set[str] = set()
dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
ihook = self.ihook
for dic in dicts:
values: List[Union[nodes.Item, nodes.Collector]] = []
# Note: seems like the dict can change during iteration -
# be careful not to remove the list() without consideration.
for name, obj in list(dic.items()):
if name in IGNORED_ATTRIBUTES:
continue
if name in seen:
continue
seen.add(name)
res = ihook.pytest_pycollect_makeitem(
collector=self, name=name, obj=obj
)
if res is None:
continue
elif isinstance(res, list):
values.extend(res)
else:
values.append(res)
dict_values.append(values)
# Between classes in the class hierarchy, reverse-MRO order -- nodes
# inherited from base classes should come before subclasses.
result = []
for values in reversed(dict_values):
result.extend(values)
return result
def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
modulecol = self.getparent(Module)
assert modulecol is not None
module = modulecol.obj
clscol = self.getparent(Class)
cls = clscol and clscol.obj or None
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
fixtureinfo = definition._fixtureinfo
# pytest_generate_tests impls call metafunc.parametrize() which fills
# metafunc._calls, the outcome of the hook.
metafunc = Metafunc(
definition=definition,
fixtureinfo=fixtureinfo,
config=self.config,
cls=cls,
module=module,
_ispytest=True,
)
methods = []
if hasattr(module, "pytest_generate_tests"):
methods.append(module.pytest_generate_tests)
if cls is not None and hasattr(cls, "pytest_generate_tests"):
methods.append(cls().pytest_generate_tests)
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
if not metafunc._calls:
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
else:
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
fm = self.session._fixturemanager
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
# with direct parametrization, so make sure we update what the
# function really needs.
fixtureinfo.prune_dependency_tree()
for callspec in metafunc._calls:
subname = f"{name}[{callspec.id}]"
yield Function.from_parent(
self,
name=subname,
callspec=callspec,
fixtureinfo=fixtureinfo,
keywords={callspec.id: True},
originalname=name,
)
class Module(nodes.File, PyCollector):
"""Collector for test classes and functions."""
def _getobj(self):
return self._importtestmodule()
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
return super().collect()
def _inject_setup_module_fixture(self) -> None:
"""Inject a hidden autouse, module scoped fixture into the collected module object
that invokes setUpModule/tearDownModule if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
has_nose = self.config.pluginmanager.has_plugin("nose")
setup_module = _get_first_non_fixture_func(
self.obj, ("setUpModule", "setup_module")
)
if setup_module is None and has_nose:
# The name "setup" is too common - only treat as fixture if callable.
setup_module = _get_first_non_fixture_func(self.obj, ("setup",))
if not callable(setup_module):
setup_module = None
teardown_module = _get_first_non_fixture_func(
self.obj, ("tearDownModule", "teardown_module")
)
if teardown_module is None and has_nose:
teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",))
# Same as "setup" above - only treat as fixture if callable.
if not callable(teardown_module):
teardown_module = None
if setup_module is None and teardown_module is None:
return
@fixtures.fixture(
autouse=True,
scope="module",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
)
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
if setup_module is not None:
_call_with_optional_argument(setup_module, request.module)
yield
if teardown_module is not None:
_call_with_optional_argument(teardown_module, request.module)
self.obj.__pytest_setup_module = xunit_setup_module_fixture
def _inject_setup_function_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected module object
that invokes setup_function/teardown_function if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",))
teardown_function = _get_first_non_fixture_func(
self.obj, ("teardown_function",)
)
if setup_function is None and teardown_function is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
)
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
if request.instance is not None:
# in this case we are bound to an instance, so we need to let
# setup_method handle this
yield
return
if setup_function is not None:
_call_with_optional_argument(setup_function, request.function)
yield
if teardown_function is not None:
_call_with_optional_argument(teardown_function, request.function)
self.obj.__pytest_setup_function = xunit_setup_function_fixture
def _importtestmodule(self):
# We assume we are only called once per module.
importmode = self.config.getoption("--import-mode")
try:
mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
except SyntaxError as e:
raise self.CollectError(
ExceptionInfo.from_current().getrepr(style="short")
) from e
except ImportPathMismatchError as e:
raise self.CollectError(
"import file mismatch:\n"
"imported module %r has this __file__ attribute:\n"
" %s\n"
"which is not the same as the test file we want to collect:\n"
" %s\n"
"HINT: remove __pycache__ / .pyc files and/or use a "
"unique basename for your test file modules" % e.args
) from e
except ImportError as e:
exc_info = ExceptionInfo.from_current()
if self.config.getoption("verbose") < 2:
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
exc_repr = (
exc_info.getrepr(style="short")
if exc_info.traceback
else exc_info.exconly()
)
formatted_tb = str(exc_repr)
raise self.CollectError(
"ImportError while importing test module '{path}'.\n"
"Hint: make sure your test modules/packages have valid Python names.\n"
"Traceback:\n"
"{traceback}".format(path=self.path, traceback=formatted_tb)
) from e
except skip.Exception as e:
if e.allow_module_level:
raise
raise self.CollectError(
"Using pytest.skip outside of a test will skip the entire module. "
"If that's your intention, pass `allow_module_level=True`. "
"If you want to skip a specific test or an entire class, "
"use the @pytest.mark.skip or @pytest.mark.skipif decorators."
) from e
self.config.pluginmanager.consider_module(mod)
return mod
class Package(Module):
def __init__(
self,
fspath: Optional[LEGACY_PATH],
parent: nodes.Collector,
# NOTE: following args are unused:
config=None,
session=None,
nodeid=None,
path=Optional[Path],
) -> None:
# NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent)
session = parent.session
nodes.FSCollector.__init__(
self,
fspath=fspath,
path=path,
parent=parent,
config=config,
session=session,
nodeid=nodeid,
)
self.name = self.path.parent.name
def setup(self) -> None:
# Not using fixtures to call setup_module here because autouse fixtures
# from packages are not called automatically (#4085).
setup_module = _get_first_non_fixture_func(
self.obj, ("setUpModule", "setup_module")
)
if setup_module is not None:
_call_with_optional_argument(setup_module, self.obj)
teardown_module = _get_first_non_fixture_func(
self.obj, ("tearDownModule", "teardown_module")
)
if teardown_module is not None:
func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func)
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.isinitpath(path)
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__":
return False
fspath = Path(direntry.path)
ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True
def _collectfile(
self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]:
assert (
fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
)
ihook = self.session.gethookproxy(fspath)
if not self.session.isinitpath(fspath):
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return ()
if handle_dupes:
keepduplicates = self.config.getoption("keepduplicates")
if not keepduplicates:
duplicate_paths = self.config.pluginmanager._duplicatepaths
if fspath in duplicate_paths:
return ()
else:
duplicate_paths.add(fspath)
return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return]
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent
init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns(
init_module, self.config.getini("python_files")
):
yield Module.from_parent(self, path=init_module)
pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path)
# We will visit our own __init__.py file, in which case we skip it.
if direntry.is_file():
if direntry.name == "__init__.py" and path.parent == this_path:
continue
parts_ = parts(direntry.path)
if any(
str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path
for pkg_prefix in pkg_prefixes
):
continue
if direntry.is_file():
yield from self._collectfile(path)
elif not direntry.is_dir():
# Broken symlink or invalid/missing file.
continue
elif path.joinpath("__init__.py").is_file():
pkg_prefixes.add(path)
def _call_with_optional_argument(func, arg) -> None:
"""Call the given function with the given argument if func accepts one argument, otherwise
calls func without arguments."""
arg_count = func.__code__.co_argcount
if inspect.ismethod(func):
arg_count -= 1
if arg_count:
func(arg)
else:
func()
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
"""Return the attribute from the given object to be used as a setup/teardown
xunit-style function, but only if not marked as a fixture to avoid calling it twice.
"""
for name in names:
meth: Optional[object] = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None:
return meth
return None
class Class(PyCollector):
"""Collector for test methods."""
@classmethod
def from_parent(cls, parent, *, name, obj=None, **kw):
"""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 []
if hasinit(self.obj):
assert self.parent is not None
self.warn(
PytestCollectionWarning(
"cannot collect test class %r because it has a "
"__init__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
)
)
return []
elif hasnew(self.obj):
assert self.parent is not None
self.warn(
PytestCollectionWarning(
"cannot collect test class %r because it has a "
"__new__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
)
)
return []
self._inject_setup_class_fixture()
self._inject_setup_method_fixture()
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
that invokes setup_class/teardown_class if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
if setup_class is None and teardown_class is None:
return
@fixtures.fixture(
autouse=True,
scope="class",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
)
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
if setup_class is not None:
func = getimfunc(setup_class)
_call_with_optional_argument(func, self.obj)
yield
if teardown_class is not None:
func = getimfunc(teardown_class)
_call_with_optional_argument(func, self.obj)
self.obj.__pytest_setup_class = xunit_setup_class_fixture
def _inject_setup_method_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
has_nose = self.config.pluginmanager.has_plugin("nose")
setup_name = "setup_method"
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
emit_nose_setup_warning = False
if setup_method is None and has_nose:
setup_name = "setup"
emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method"
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
emit_nose_teardown_warning = False
if teardown_method is None and has_nose:
teardown_name = "teardown"
emit_nose_teardown_warning = True
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
if setup_method is None and teardown_method is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
)
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
method = request.function
if setup_method is not None:
func = getattr(self, setup_name)
_call_with_optional_argument(func, method)
if emit_nose_setup_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="setup"
),
stacklevel=2,
)
yield
if teardown_method is not None:
func = getattr(self, teardown_name)
_call_with_optional_argument(func, method)
if emit_nose_teardown_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="teardown"
),
stacklevel=2,
)
self.obj.__pytest_setup_method = xunit_setup_method_fixture
class InstanceDummy: class InstanceDummy:
"""Instance used to be a node type between Class and Function. It has been """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` removed in pytest 7.0. Some plugins exist which reference `pytest.Instance`
@ -917,171 +232,7 @@ def __getattr__(name: str) -> object:
raise AttributeError(f"module {__name__} has no attribute {name}") raise AttributeError(f"module {__name__} has no attribute {name}")
def hasinit(obj: object) -> bool: class FunctionDefinition(nodes.Function):
init: object = getattr(obj, "__init__", None)
if init:
return init != object.__init__
return False
def hasnew(obj: object) -> bool:
new: object = getattr(obj, "__new__", None)
if new:
return new != object.__new__
return False
class Function(PyobjMixin, nodes.Item):
"""An Item responsible for setting up and executing a Python test function.
:param name:
The full function name, including any decorations like those
added by parametrization (``my_func[my_param]``).
:param parent:
The parent Node.
:param config:
The pytest Config object.
:param callspec:
If given, this is function has been parametrized and the callspec contains
meta information about the parametrization.
:param callobj:
If given, the object which will be called when the Function is invoked,
otherwise the callobj will be obtained from ``parent`` using ``originalname``.
:param keywords:
Keywords bound to the function object for "-k" matching.
:param session:
The pytest Session object.
:param fixtureinfo:
Fixture information already resolved at this fixture node..
:param originalname:
The attribute name to use for accessing the underlying function object.
Defaults to ``name``. Set this if name is different from the original name,
for example when it contains decorations like those added by parametrization
(``my_func[my_param]``).
"""
# Disable since functions handle it themselves.
_ALLOW_MARKERS = False
def __init__(
self,
name: str,
parent,
config: Optional[Config] = None,
callspec: Optional[CallSpec2] = None,
callobj=NOTSET,
keywords: Optional[Mapping[str, Any]] = None,
session: Optional[Session] = None,
fixtureinfo: Optional[FuncFixtureInfo] = None,
originalname: Optional[str] = None,
) -> None:
super().__init__(name, parent, config=config, session=session)
if callobj is not NOTSET:
self.obj = callobj
#: Original function name, without any decorations (for example
#: parametrization adds a ``"[...]"`` suffix to function names), used to access
#: the underlying function object from ``parent`` (in case ``callobj`` is not given
#: explicitly).
#:
#: .. versionadded:: 3.0
self.originalname = originalname or name
# Note: when FunctionDefinition is introduced, we should change ``originalname``
# to a readonly property that returns FunctionDefinition.name.
self.own_markers.extend(get_unpacked_marks(self.obj))
if callspec:
self.callspec = callspec
self.own_markers.extend(callspec.marks)
# todo: this is a hell of a hack
# https://github.com/pytest-dev/pytest/issues/4569
# Note: the order of the updates is important here; indicates what
# takes priority (ctor argument over function attributes over markers).
# Take own_markers only; NodeKeywords handles parent traversal on its own.
self.keywords.update((mark.name, mark) for mark in self.own_markers)
self.keywords.update(self.obj.__dict__)
if keywords:
self.keywords.update(keywords)
if fixtureinfo is None:
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
self, self.obj, self.cls, funcargs=True
)
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
self.fixturenames = fixtureinfo.names_closure
self._initrequest()
@classmethod
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
"""The public constructor."""
return super().from_parent(parent=parent, **kw)
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self._request = fixtures.FixtureRequest(self, _ispytest=True)
@property
def function(self):
"""Underlying python 'function' object."""
return getimfunc(self.obj)
def _getobj(self):
assert self.parent is not None
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):
"""(compatonly) for code expecting pytest-2.2 style request objects."""
return self
def runtest(self) -> None:
"""Execute the underlying test function."""
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
def setup(self) -> None:
self._request._fillfixtures()
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code.from_function(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno
traceback = excinfo.traceback
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
if ntraceback == traceback:
ntraceback = ntraceback.cut(path=path)
if ntraceback == traceback:
ntraceback = ntraceback.filter(filter_traceback)
if not ntraceback:
ntraceback = traceback
excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to
# only show a single-line message for each frame.
if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2:
for entry in excinfo.traceback[1:-1]:
entry.set_repr_style("short")
# TODO: Type ignored -- breaks Liskov Substitution.
def repr_failure( # type: ignore[override]
self,
excinfo: ExceptionInfo[BaseException],
) -> Union[str, TerminalRepr]:
style = self.config.getoption("tbstyle", "auto")
if style == "auto":
style = "long"
return self._repr_failure_py(excinfo, style=style)
class FunctionDefinition(Function):
""" """
This class is a step gap solution until we evolve to have actual function definition nodes This class is a step gap solution until we evolve to have actual function definition nodes
and manage to get rid of ``metafunc``. and manage to get rid of ``metafunc``.

View File

@ -15,8 +15,8 @@ from typing import Sequence
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
from . import nodes
from _pytest import fixtures from _pytest import fixtures
from _pytest import python
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped from _pytest.compat import ascii_escaped
from _pytest.compat import assert_never from _pytest.compat import assert_never
@ -300,7 +300,7 @@ class Metafunc:
def __init__( def __init__(
self, self,
definition: "python.FunctionDefinition", definition: nodes.FunctionDefinition,
fixtureinfo: fixtures.FuncFixtureInfo, fixtureinfo: fixtures.FuncFixtureInfo,
config: Config, config: Config,
cls=None, cls=None,

881
src/_pytest/python/nodes.py Normal file
View File

@ -0,0 +1,881 @@
import fnmatch
import inspect
import os
import sys
import types
import warnings
from functools import partial
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Generator
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Union
import _pytest
from .metafunc import CallSpec2
from .metafunc import Metafunc
from _pytest import fixtures
from _pytest import nodes
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._code import getfslineno
from _pytest._code.code import TerminalRepr
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.compat import LEGACY_PATH
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.config import Config
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo
from _pytest.main import Session
from _pytest.mark.structures import get_unpacked_marks
from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import parts
from _pytest.pathlib import visit
from _pytest.warning_types import PytestCollectionWarning
def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
"""Return whether path matches any of the patterns in the list of globs given."""
return any(fnmatch_ex(pattern, path) for pattern in patterns)
class PyobjMixin(nodes.Node):
"""this mix-in inherits from Node to carry over the typing information
as its intended to always mix in before a node
its position in the mro is unaffected"""
_ALLOW_MARKERS = True
@property
def module(self):
"""Python module object this node was collected from (can be None)."""
node = self.getparent(Module)
return node.obj if node is not None else None
@property
def cls(self):
"""Python class object this node was collected from (can be None)."""
node = self.getparent(Class)
return node.obj if node is not None else None
@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,
a staticmethod, a class or a module.
"""
node = self.getparent(Function)
return getattr(node.obj, "__self__", None) if node is not None else None
@property
def obj(self):
"""Underlying Python object."""
obj = getattr(self, "_obj", None)
if obj is None:
self._obj = obj = self._getobj()
# XXX evil hack
# used to avoid Function marker duplication
if self._ALLOW_MARKERS:
self.own_markers.extend(get_unpacked_marks(self.obj))
# This assumes that `obj` is called before there is a chance
# to add custom keys to `self.keywords`, so no fear of overriding.
self.keywords.update((mark.name, mark) for mark in self.own_markers)
return obj
@obj.setter
def obj(self, value):
self._obj = value
def _getobj(self):
"""Get the underlying Python object. May be overwritten by subclasses."""
# 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 getattr(obj, self.name)
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
"""Return Python path relative to the containing module."""
chain = self.listchain()
chain.reverse()
parts = []
for node in chain:
name = node.name
if isinstance(node, Module):
name = os.path.splitext(name)[0]
if stopatmodule:
if includemodule:
parts.append(name)
break
parts.append(name)
parts.reverse()
return ".".join(parts)
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
# XXX caching?
obj = self.obj
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
if isinstance(compat_co_firstlineno, int):
# nose compatibility
file_path = sys.modules[obj.__module__].__file__
assert file_path is not None
if file_path.endswith(".pyc"):
file_path = file_path[:-1]
path: Union["os.PathLike[str]", str] = file_path
lineno = compat_co_firstlineno
else:
path, lineno = getfslineno(obj)
modpath = self.getmodpath()
assert isinstance(lineno, int)
return path, lineno, modpath
class _EmptyClass:
pass # noqa: E701
IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
frozenset(),
# Module.
dir(types.ModuleType("empty_module")),
# Some extra module attributes the above doesn't catch.
{"__builtins__", "__file__", "__cached__"},
# Class.
dir(_EmptyClass),
# Instance.
dir(_EmptyClass()),
)
class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name)
def isnosetest(self, obj: object) -> bool:
"""Look for the __test__ attribute, which is applied by the
@nose.tools.istest decorator.
"""
# We explicitly check for "is True" here to not mistakenly treat
# classes with a custom __getattr__ returning something truthy (like a
# function) as test classes.
return safe_getattr(obj, "__test__", False) is True
def classnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_classes", name)
def istestfunction(self, obj: object, name: str) -> bool:
if self.funcnamefilter(name) or self.isnosetest(obj):
if isinstance(obj, (staticmethod, classmethod)):
# staticmethods and classmethods need to be unwrapped.
obj = safe_getattr(obj, "__func__", False)
return callable(obj) and fixtures.getfixturemarker(obj) is None
else:
return False
def istestclass(self, obj: object, name: str) -> bool:
return self.classnamefilter(name) or self.isnosetest(obj)
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
"""Check if the given name matches the prefix or glob-pattern defined
in ini configuration."""
for option in self.config.getini(option_name):
if name.startswith(option):
return True
# Check that name looks like a glob-string before calling fnmatch
# because this is called for every name in each collected module,
# and fnmatch is somewhat expensive to call.
elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
name, option
):
return True
return False
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
if not getattr(self.obj, "__test__", True):
return []
# Avoid random getattrs and peek in the __dict__ instead.
dicts = [getattr(self.obj, "__dict__", {})]
if isinstance(self.obj, type):
for basecls in self.obj.__mro__:
dicts.append(basecls.__dict__)
# In each class, nodes should be definition ordered.
# __dict__ is definition ordered.
seen: Set[str] = set()
dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
ihook = self.ihook
for dic in dicts:
values: List[Union[nodes.Item, nodes.Collector]] = []
# Note: seems like the dict can change during iteration -
# be careful not to remove the list() without consideration.
for name, obj in list(dic.items()):
if name in IGNORED_ATTRIBUTES:
continue
if name in seen:
continue
seen.add(name)
res = ihook.pytest_pycollect_makeitem(
collector=self, name=name, obj=obj
)
if res is None:
continue
elif isinstance(res, list):
values.extend(res)
else:
values.append(res)
dict_values.append(values)
# Between classes in the class hierarchy, reverse-MRO order -- nodes
# inherited from base classes should come before subclasses.
result = []
for values in reversed(dict_values):
result.extend(values)
return result
def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
modulecol = self.getparent(Module)
assert modulecol is not None
module = modulecol.obj
clscol = self.getparent(Class)
cls = clscol and clscol.obj or None
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
fixtureinfo = definition._fixtureinfo
# pytest_generate_tests impls call metafunc.parametrize() which fills
# metafunc._calls, the outcome of the hook.
metafunc = Metafunc(
definition=definition,
fixtureinfo=fixtureinfo,
config=self.config,
cls=cls,
module=module,
_ispytest=True,
)
methods = []
if hasattr(module, "pytest_generate_tests"):
methods.append(module.pytest_generate_tests)
if cls is not None and hasattr(cls, "pytest_generate_tests"):
methods.append(cls().pytest_generate_tests)
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
if not metafunc._calls:
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
else:
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
fm = self.session._fixturemanager
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
# with direct parametrization, so make sure we update what the
# function really needs.
fixtureinfo.prune_dependency_tree()
for callspec in metafunc._calls:
subname = f"{name}[{callspec.id}]"
yield Function.from_parent(
self,
name=subname,
callspec=callspec,
fixtureinfo=fixtureinfo,
keywords={callspec.id: True},
originalname=name,
)
class Module(nodes.File, PyCollector):
"""Collector for test classes and functions."""
def _getobj(self):
return self._importtestmodule()
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
return super().collect()
def _inject_setup_module_fixture(self) -> None:
"""Inject a hidden autouse, module scoped fixture into the collected module object
that invokes setUpModule/tearDownModule if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
has_nose = self.config.pluginmanager.has_plugin("nose")
setup_module = _get_first_non_fixture_func(
self.obj, ("setUpModule", "setup_module")
)
if setup_module is None and has_nose:
# The name "setup" is too common - only treat as fixture if callable.
setup_module = _get_first_non_fixture_func(self.obj, ("setup",))
if not callable(setup_module):
setup_module = None
teardown_module = _get_first_non_fixture_func(
self.obj, ("tearDownModule", "teardown_module")
)
if teardown_module is None and has_nose:
teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",))
# Same as "setup" above - only treat as fixture if callable.
if not callable(teardown_module):
teardown_module = None
if setup_module is None and teardown_module is None:
return
@fixtures.fixture(
autouse=True,
scope="module",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
)
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
if setup_module is not None:
_call_with_optional_argument(setup_module, request.module)
yield
if teardown_module is not None:
_call_with_optional_argument(teardown_module, request.module)
self.obj.__pytest_setup_module = xunit_setup_module_fixture
def _inject_setup_function_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected module object
that invokes setup_function/teardown_function if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",))
teardown_function = _get_first_non_fixture_func(
self.obj, ("teardown_function",)
)
if setup_function is None and teardown_function is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
)
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
if request.instance is not None:
# in this case we are bound to an instance, so we need to let
# setup_method handle this
yield
return
if setup_function is not None:
_call_with_optional_argument(setup_function, request.function)
yield
if teardown_function is not None:
_call_with_optional_argument(teardown_function, request.function)
self.obj.__pytest_setup_function = xunit_setup_function_fixture
def _importtestmodule(self):
# We assume we are only called once per module.
importmode = self.config.getoption("--import-mode")
try:
mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
except SyntaxError as e:
raise self.CollectError(
ExceptionInfo.from_current().getrepr(style="short")
) from e
except ImportPathMismatchError as e:
raise self.CollectError(
"import file mismatch:\n"
"imported module %r has this __file__ attribute:\n"
" %s\n"
"which is not the same as the test file we want to collect:\n"
" %s\n"
"HINT: remove __pycache__ / .pyc files and/or use a "
"unique basename for your test file modules" % e.args
) from e
except ImportError as e:
exc_info = ExceptionInfo.from_current()
if self.config.getoption("verbose") < 2:
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
exc_repr = (
exc_info.getrepr(style="short")
if exc_info.traceback
else exc_info.exconly()
)
formatted_tb = str(exc_repr)
raise self.CollectError(
"ImportError while importing test module '{path}'.\n"
"Hint: make sure your test modules/packages have valid Python names.\n"
"Traceback:\n"
"{traceback}".format(path=self.path, traceback=formatted_tb)
) from e
except skip.Exception as e:
if e.allow_module_level:
raise
raise self.CollectError(
"Using pytest.skip outside of a test will skip the entire module. "
"If that's your intention, pass `allow_module_level=True`. "
"If you want to skip a specific test or an entire class, "
"use the @pytest.mark.skip or @pytest.mark.skipif decorators."
) from e
self.config.pluginmanager.consider_module(mod)
return mod
class Package(Module):
def __init__(
self,
fspath: Optional[LEGACY_PATH],
parent: nodes.Collector,
# NOTE: following args are unused:
config=None,
session=None,
nodeid=None,
path=Optional[Path],
) -> None:
# NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent)
session = parent.session
nodes.FSCollector.__init__(
self,
fspath=fspath,
path=path,
parent=parent,
config=config,
session=session,
nodeid=nodeid,
)
self.name = self.path.parent.name
def setup(self) -> None:
# Not using fixtures to call setup_module here because autouse fixtures
# from packages are not called automatically (#4085).
setup_module = _get_first_non_fixture_func(
self.obj, ("setUpModule", "setup_module")
)
if setup_module is not None:
_call_with_optional_argument(setup_module, self.obj)
teardown_module = _get_first_non_fixture_func(
self.obj, ("tearDownModule", "teardown_module")
)
if teardown_module is not None:
func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func)
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.isinitpath(path)
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__":
return False
fspath = Path(direntry.path)
ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True
def _collectfile(
self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]:
assert (
fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
)
ihook = self.session.gethookproxy(fspath)
if not self.session.isinitpath(fspath):
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return ()
if handle_dupes:
keepduplicates = self.config.getoption("keepduplicates")
if not keepduplicates:
duplicate_paths = self.config.pluginmanager._duplicatepaths
if fspath in duplicate_paths:
return ()
else:
duplicate_paths.add(fspath)
return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return]
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent
init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns(
init_module, self.config.getini("python_files")
):
yield Module.from_parent(self, path=init_module)
pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path)
# We will visit our own __init__.py file, in which case we skip it.
if direntry.is_file():
if direntry.name == "__init__.py" and path.parent == this_path:
continue
parts_ = parts(direntry.path)
if any(
str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path
for pkg_prefix in pkg_prefixes
):
continue
if direntry.is_file():
yield from self._collectfile(path)
elif not direntry.is_dir():
# Broken symlink or invalid/missing file.
continue
elif path.joinpath("__init__.py").is_file():
pkg_prefixes.add(path)
def _call_with_optional_argument(func, arg) -> None:
"""Call the given function with the given argument if func accepts one argument, otherwise
calls func without arguments."""
arg_count = func.__code__.co_argcount
if inspect.ismethod(func):
arg_count -= 1
if arg_count:
func(arg)
else:
func()
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
"""Return the attribute from the given object to be used as a setup/teardown
xunit-style function, but only if not marked as a fixture to avoid calling it twice.
"""
for name in names:
meth: Optional[object] = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None:
return meth
return None
class Class(PyCollector):
"""Collector for test methods."""
@classmethod
def from_parent(cls, parent, *, name, obj=None, **kw):
"""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 []
if hasinit(self.obj):
assert self.parent is not None
self.warn(
PytestCollectionWarning(
"cannot collect test class %r because it has a "
"__init__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
)
)
return []
elif hasnew(self.obj):
assert self.parent is not None
self.warn(
PytestCollectionWarning(
"cannot collect test class %r because it has a "
"__new__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
)
)
return []
self._inject_setup_class_fixture()
self._inject_setup_method_fixture()
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
that invokes setup_class/teardown_class if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
if setup_class is None and teardown_class is None:
return
@fixtures.fixture(
autouse=True,
scope="class",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
)
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
if setup_class is not None:
func = getimfunc(setup_class)
_call_with_optional_argument(func, self.obj)
yield
if teardown_class is not None:
func = getimfunc(teardown_class)
_call_with_optional_argument(func, self.obj)
self.obj.__pytest_setup_class = xunit_setup_class_fixture
def _inject_setup_method_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
has_nose = self.config.pluginmanager.has_plugin("nose")
setup_name = "setup_method"
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
emit_nose_setup_warning = False
if setup_method is None and has_nose:
setup_name = "setup"
emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method"
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
emit_nose_teardown_warning = False
if teardown_method is None and has_nose:
teardown_name = "teardown"
emit_nose_teardown_warning = True
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
if setup_method is None and teardown_method is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
)
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
method = request.function
if setup_method is not None:
func = getattr(self, setup_name)
_call_with_optional_argument(func, method)
if emit_nose_setup_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="setup"
),
stacklevel=2,
)
yield
if teardown_method is not None:
func = getattr(self, teardown_name)
_call_with_optional_argument(func, method)
if emit_nose_teardown_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="teardown"
),
stacklevel=2,
)
self.obj.__pytest_setup_method = xunit_setup_method_fixture
def hasinit(obj: object) -> bool:
init: object = getattr(obj, "__init__", None)
if init:
return init != object.__init__
return False
def hasnew(obj: object) -> bool:
new: object = getattr(obj, "__new__", None)
if new:
return new != object.__new__
return False
class Function(PyobjMixin, nodes.Item):
"""An Item responsible for setting up and executing a Python test function.
:param name:
The full function name, including any decorations like those
added by parametrization (``my_func[my_param]``).
:param parent:
The parent Node.
:param config:
The pytest Config object.
:param callspec:
If given, this is function has been parametrized and the callspec contains
meta information about the parametrization.
:param callobj:
If given, the object which will be called when the Function is invoked,
otherwise the callobj will be obtained from ``parent`` using ``originalname``.
:param keywords:
Keywords bound to the function object for "-k" matching.
:param session:
The pytest Session object.
:param fixtureinfo:
Fixture information already resolved at this fixture node..
:param originalname:
The attribute name to use for accessing the underlying function object.
Defaults to ``name``. Set this if name is different from the original name,
for example when it contains decorations like those added by parametrization
(``my_func[my_param]``).
"""
# Disable since functions handle it themselves.
_ALLOW_MARKERS = False
def __init__(
self,
name: str,
parent,
config: Optional[Config] = None,
callspec: Optional[CallSpec2] = None,
callobj=NOTSET,
keywords: Optional[Mapping[str, Any]] = None,
session: Optional[Session] = None,
fixtureinfo: Optional[FuncFixtureInfo] = None,
originalname: Optional[str] = None,
) -> None:
super().__init__(name, parent, config=config, session=session)
if callobj is not NOTSET:
self.obj = callobj
#: Original function name, without any decorations (for example
#: parametrization adds a ``"[...]"`` suffix to function names), used to access
#: the underlying function object from ``parent`` (in case ``callobj`` is not given
#: explicitly).
#:
#: .. versionadded:: 3.0
self.originalname = originalname or name
# Note: when FunctionDefinition is introduced, we should change ``originalname``
# to a readonly property that returns FunctionDefinition.name.
self.own_markers.extend(get_unpacked_marks(self.obj))
if callspec:
self.callspec = callspec
self.own_markers.extend(callspec.marks)
# todo: this is a hell of a hack
# https://github.com/pytest-dev/pytest/issues/4569
# Note: the order of the updates is important here; indicates what
# takes priority (ctor argument over function attributes over markers).
# Take own_markers only; NodeKeywords handles parent traversal on its own.
self.keywords.update((mark.name, mark) for mark in self.own_markers)
self.keywords.update(self.obj.__dict__)
if keywords:
self.keywords.update(keywords)
if fixtureinfo is None:
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
self, self.obj, self.cls, funcargs=True
)
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
self.fixturenames = fixtureinfo.names_closure
self._initrequest()
@classmethod
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
"""The public constructor."""
return super().from_parent(parent=parent, **kw)
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self._request = fixtures.FixtureRequest(self, _ispytest=True)
@property
def function(self):
"""Underlying python 'function' object."""
return getimfunc(self.obj)
def _getobj(self):
assert self.parent is not None
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):
"""(compatonly) for code expecting pytest-2.2 style request objects."""
return self
def runtest(self) -> None:
"""Execute the underlying test function."""
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
def setup(self) -> None:
self._request._fillfixtures()
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code.from_function(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno
traceback = excinfo.traceback
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
if ntraceback == traceback:
ntraceback = ntraceback.cut(path=path)
if ntraceback == traceback:
ntraceback = ntraceback.filter(filter_traceback)
if not ntraceback:
ntraceback = traceback
excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to
# only show a single-line message for each frame.
if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2:
for entry in excinfo.traceback[1:-1]:
entry.set_repr_style("short")
# TODO: Type ignored -- breaks Liskov Substitution.
def repr_failure( # type: ignore[override]
self,
excinfo: ExceptionInfo[BaseException],
) -> Union[str, TerminalRepr]:
style = self.config.getoption("tbstyle", "auto")
if style == "auto":
style = "long"
return self._repr_failure_py(excinfo, style=style)
class FunctionDefinition(Function):
"""
This class is a step gap solution until we evolve to have actual function definition nodes
and manage to get rid of ``metafunc``.
"""
def runtest(self) -> None:
raise RuntimeError("function definitions are not supposed to be run as tests")
setup = runtest

View File

@ -39,6 +39,7 @@ from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
tmppath_result_key = StashKey[Dict[str, bool]]() tmppath_result_key = StashKey[Dict[str, bool]]()
tmppath_factory_key = StashKey["TempPathFactory"]()
@final @final
@ -220,6 +221,7 @@ def pytest_configure(config: Config) -> None:
available at pytest_configure time, but ideally should be moved entirely available at pytest_configure time, but ideally should be moved entirely
to the tmp_path_factory session fixture. to the tmp_path_factory session fixture.
""" """
config.stash[tmppath_factory_key]
mp = MonkeyPatch() mp = MonkeyPatch()
config.add_cleanup(mp.undo) config.add_cleanup(mp.undo)
_tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True) _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
@ -245,7 +247,7 @@ def pytest_addoption(parser: Parser) -> None:
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
"""Return a :class:`pytest.TempPathFactory` instance for the test session.""" """Return a :class:`pytest.TempPathFactory` instance for the test session."""
# Set dynamically by pytest_configure() above. # Set dynamically by pytest_configure() above.
return request.config._tmp_path_factory # type: ignore return request.config.stash[tmppath_factory_key]
def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:

View File

@ -25,9 +25,9 @@ from _pytest.outcomes import exit
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.outcomes import xfail from _pytest.outcomes import xfail
from _pytest.python import Class from _pytest.python.nodes import Class
from _pytest.python import Function from _pytest.python.nodes import Function
from _pytest.python import Module from _pytest.python.nodes import Module
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.scope import Scope from _pytest.scope import Scope

View File

@ -47,10 +47,6 @@ from _pytest.pytester import LineMatcher
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.pytester import RecordedHookCall from _pytest.pytester import RecordedHookCall
from _pytest.pytester import RunResult from _pytest.pytester import RunResult
from _pytest.python import Class
from _pytest.python import Function
from _pytest.python import Module
from _pytest.python import Package
from _pytest.python_api import approx from _pytest.python_api import approx
from _pytest.python_api import raises from _pytest.python_api import raises
from _pytest.recwarn import deprecated_call from _pytest.recwarn import deprecated_call
@ -85,7 +81,6 @@ __all__ = [
"Cache", "Cache",
"CallInfo", "CallInfo",
"CaptureFixture", "CaptureFixture",
"Class",
"cmdline", "cmdline",
"Collector", "Collector",
"CollectReport", "CollectReport",
@ -102,7 +97,6 @@ __all__ = [
"FixtureLookupError", "FixtureLookupError",
"FixtureRequest", "FixtureRequest",
"freeze_includes", "freeze_includes",
"Function",
"hookimpl", "hookimpl",
"HookRecorder", "HookRecorder",
"hookspec", "hookspec",
@ -115,10 +109,8 @@ __all__ = [
"Mark", "Mark",
"MarkDecorator", "MarkDecorator",
"MarkGenerator", "MarkGenerator",
"Module",
"MonkeyPatch", "MonkeyPatch",
"OptionGroup", "OptionGroup",
"Package",
"param", "param",
"Parser", "Parser",
"PytestAssertRewriteWarning", "PytestAssertRewriteWarning",

View File

@ -3,6 +3,7 @@ import sys
import warnings import warnings
from pathlib import Path from pathlib import Path
import _pytest.python.nodes
import pytest import pytest
from _pytest import deprecated from _pytest import deprecated
from _pytest.compat import legacy_path from _pytest.compat import legacy_path
@ -75,9 +76,9 @@ def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None:
""", """,
withinit=True, withinit=True,
) )
assert isinstance(module, pytest.Module) assert isinstance(module, _pytest.python.nodes.Module)
package = module.parent package = module.parent
assert isinstance(package, pytest.Package) assert isinstance(package, _pytest.python.nodes.Package)
with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"): with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"):
package.gethookproxy(pytester.path) package.gethookproxy(pytester.path)

View File

@ -5,14 +5,15 @@ from typing import Any
from typing import Dict from typing import Dict
import _pytest._code import _pytest._code
import _pytest.python.nodes
import pytest import pytest
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.main import Session from _pytest.main import Session
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.python import Class from _pytest.python.nodes import Class
from _pytest.python import Function from _pytest.python.nodes import Function
class TestModule: class TestModule:
@ -266,8 +267,8 @@ class TestClass:
class TestFunction: class TestFunction:
def test_getmodulecollector(self, pytester: Pytester) -> None: def test_getmodulecollector(self, pytester: Pytester) -> None:
item = pytester.getitem("def test_func(): pass") item = pytester.getitem("def test_func(): pass")
modcol = item.getparent(pytest.Module) modcol = item.getparent(_pytest.python.nodes.Module)
assert isinstance(modcol, pytest.Module) assert isinstance(modcol, _pytest.python.nodes.Module)
assert hasattr(modcol.obj, "test_func") assert hasattr(modcol.obj, "test_func")
@pytest.mark.filterwarnings("default") @pytest.mark.filterwarnings("default")
@ -298,7 +299,7 @@ class TestFunction:
session = Session.from_config(config) session = Session.from_config(config)
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
return pytest.Function.from_parent(parent=session, **kwargs) return _pytest.python.nodes.Function.from_parent(parent=session, **kwargs)
def test_function_equality(self, pytester: Pytester) -> None: def test_function_equality(self, pytester: Pytester) -> None:
def func1(): def func1():
@ -696,7 +697,7 @@ class TestFunction:
) )
originalnames = [] originalnames = []
for x in items: for x in items:
assert isinstance(x, pytest.Function) assert isinstance(x, _pytest.python.nodes.Function)
originalnames.append(x.originalname) originalnames.append(x.originalname)
assert originalnames == [ assert originalnames == [
"test_func", "test_func",
@ -729,16 +730,16 @@ class TestSorting:
""" """
) )
fn1 = pytester.collect_by_name(modcol, "test_pass") fn1 = pytester.collect_by_name(modcol, "test_pass")
assert isinstance(fn1, pytest.Function) assert isinstance(fn1, _pytest.python.nodes.Function)
fn2 = pytester.collect_by_name(modcol, "test_pass") fn2 = pytester.collect_by_name(modcol, "test_pass")
assert isinstance(fn2, pytest.Function) assert isinstance(fn2, _pytest.python.nodes.Function)
assert fn1 == fn2 assert fn1 == fn2
assert fn1 != modcol assert fn1 != modcol
assert hash(fn1) == hash(fn2) assert hash(fn1) == hash(fn2)
fn3 = pytester.collect_by_name(modcol, "test_fail") fn3 = pytester.collect_by_name(modcol, "test_fail")
assert isinstance(fn3, pytest.Function) assert isinstance(fn3, _pytest.python.nodes.Function)
assert not (fn1 == fn3) assert not (fn1 == fn3)
assert fn1 != fn3 assert fn1 != fn3

View File

@ -3,6 +3,7 @@ import sys
import textwrap import textwrap
from pathlib import Path from pathlib import Path
import _pytest.python.nodes
import pytest import pytest
from _pytest import fixtures from _pytest import fixtures
from _pytest.compat import getfuncargnames from _pytest.compat import getfuncargnames
@ -11,7 +12,7 @@ from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import get_public_names from _pytest.pytester import get_public_names
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.python import Function from _pytest.python.nodes import Function
def test_getfuncargnames_functions(): def test_getfuncargnames_functions():
@ -854,7 +855,7 @@ class TestRequestBasic:
item.session._setupstate.setup(item) item.session._setupstate.setup(item)
item._request._fillfixtures() item._request._fillfixtures()
# successively check finalization calls # successively check finalization calls
parent = item.getparent(pytest.Module) parent = item.getparent(_pytest.python.nodes.Module)
assert parent is not None assert parent is not None
teardownlist = parent.obj.teardownlist teardownlist = parent.obj.teardownlist
ss = item.session._setupstate ss = item.session._setupstate

View File

@ -2,7 +2,7 @@ import pytest
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest.fixtures import getfixturemarker from _pytest.fixtures import getfixturemarker
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.python import Function from _pytest.python.nodes import Function
def test_wrapped_getfslineno() -> None: def test_wrapped_getfslineno() -> None:

View File

@ -6,6 +6,7 @@ import textwrap
from pathlib import Path from pathlib import Path
from typing import List from typing import List
import _pytest.python.nodes
import pytest import pytest
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
@ -41,21 +42,21 @@ class TestCollector:
""" """
) )
fn1 = pytester.collect_by_name(modcol, "test_pass") fn1 = pytester.collect_by_name(modcol, "test_pass")
assert isinstance(fn1, pytest.Function) assert isinstance(fn1, _pytest.python.nodes.Function)
fn2 = pytester.collect_by_name(modcol, "test_pass") fn2 = pytester.collect_by_name(modcol, "test_pass")
assert isinstance(fn2, pytest.Function) assert isinstance(fn2, _pytest.python.nodes.Function)
assert fn1 == fn2 assert fn1 == fn2
assert fn1 != modcol assert fn1 != modcol
assert hash(fn1) == hash(fn2) assert hash(fn1) == hash(fn2)
fn3 = pytester.collect_by_name(modcol, "test_fail") fn3 = pytester.collect_by_name(modcol, "test_fail")
assert isinstance(fn3, pytest.Function) assert isinstance(fn3, _pytest.python.nodes.Function)
assert not (fn1 == fn3) assert not (fn1 == fn3)
assert fn1 != fn3 assert fn1 != fn3
for fn in fn1, fn2, fn3: for fn in fn1, fn2, fn3:
assert isinstance(fn, pytest.Function) assert isinstance(fn, _pytest.python.nodes.Function)
assert fn != 3 # type: ignore[comparison-overlap] assert fn != 3 # type: ignore[comparison-overlap]
assert fn != modcol assert fn != modcol
assert fn != [1, 2, 3] # type: ignore[comparison-overlap] assert fn != [1, 2, 3] # type: ignore[comparison-overlap]
@ -73,21 +74,21 @@ class TestCollector:
""" """
) )
cls = pytester.collect_by_name(modcol, "TestClass") cls = pytester.collect_by_name(modcol, "TestClass")
assert isinstance(cls, pytest.Class) assert isinstance(cls, _pytest.python.nodes.Class)
fn = pytester.collect_by_name(cls, "test_foo") fn = pytester.collect_by_name(cls, "test_foo")
assert isinstance(fn, pytest.Function) assert isinstance(fn, _pytest.python.nodes.Function)
assert fn.getparent(pytest.Module) is modcol assert fn.getparent(_pytest.python.nodes.Module) is modcol
assert modcol.module is not None assert modcol.module is not None
assert modcol.cls is None assert modcol.cls is None
assert modcol.instance is None assert modcol.instance is None
assert fn.getparent(pytest.Class) is cls assert fn.getparent(_pytest.python.nodes.Class) is cls
assert cls.module is not None assert cls.module is not None
assert cls.cls is not None assert cls.cls is not None
assert cls.instance is None assert cls.instance is None
assert fn.getparent(pytest.Function) is fn assert fn.getparent(_pytest.python.nodes.Function) is fn
assert fn.module is not None assert fn.module is not None
assert fn.cls is not None assert fn.cls is not None
assert fn.instance is not None assert fn.instance is not None
@ -648,7 +649,7 @@ class Test_getinitialnodes:
x = ensure_file(tmp_path / "x.py") x = ensure_file(tmp_path / "x.py")
config = pytester.parseconfigure(x) config = pytester.parseconfigure(x)
col = pytester.getnode(config, x) col = pytester.getnode(config, x)
assert isinstance(col, pytest.Module) assert isinstance(col, _pytest.python.nodes.Module)
assert col.name == "x.py" assert col.name == "x.py"
assert col.parent is not None assert col.parent is not None
assert col.parent.parent is None assert col.parent.parent is None
@ -670,8 +671,8 @@ class Test_getinitialnodes:
col = pytester.getnode(config, x) col = pytester.getnode(config, x)
assert col is not None assert col is not None
assert col.name == "x.py" assert col.name == "x.py"
assert isinstance(col, pytest.Module) assert isinstance(col, _pytest.python.nodes.Module)
assert isinstance(col.parent, pytest.Package) assert isinstance(col.parent, _pytest.python.nodes.Package)
assert isinstance(col.parent.parent, pytest.Session) assert isinstance(col.parent.parent, pytest.Session)
# session is batman (has no parents) # session is batman (has no parents)
assert col.parent.parent.parent is None assert col.parent.parent.parent is None
@ -912,10 +913,10 @@ class TestNodeKeywords:
""", """,
"test_method", "test_method",
) )
assert isinstance(item, pytest.Function) assert isinstance(item, _pytest.python.nodes.Function)
cls = item.getparent(pytest.Class) cls = item.getparent(_pytest.python.nodes.Class)
assert cls is not None assert cls is not None
mod = item.getparent(pytest.Module) mod = item.getparent(_pytest.python.nodes.Module)
assert mod is not None assert mod is not None
assert item.keywords["foo"] == pytest.mark.foo.mark assert item.keywords["foo"] == pytest.mark.foo.mark
@ -1478,7 +1479,7 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) ->
def test_class_from_parent(pytester: Pytester, request: FixtureRequest) -> None: def test_class_from_parent(pytester: Pytester, request: FixtureRequest) -> None:
"""Ensure Class.from_parent can forward custom arguments to the constructor.""" """Ensure Class.from_parent can forward custom arguments to the constructor."""
class MyCollector(pytest.Class): class MyCollector(_pytest.python.nodes.Class):
def __init__(self, name, parent, x): def __init__(self, name, parent, x):
super().__init__(name, parent) super().__init__(name, parent)
self.x = x self.x = x