123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764 |
- """Python test discovery, setup and run of test functions."""
- import enum
- import fnmatch
- import inspect
- import itertools
- import os
- import sys
- import types
- import warnings
- from collections import Counter
- from collections import defaultdict
- from functools import partial
- from pathlib import Path
- from typing import Any
- from typing import Callable
- 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 Pattern
- from typing import Sequence
- from typing import Set
- from typing import Tuple
- from typing import TYPE_CHECKING
- from typing import Union
- import attr
- import _pytest
- from _pytest import fixtures
- from _pytest import nodes
- from _pytest._code import filter_traceback
- from _pytest._code import getfslineno
- from _pytest._code.code import ExceptionInfo
- from _pytest._code.code import TerminalRepr
- from _pytest._io import TerminalWriter
- from _pytest._io.saferepr import saferepr
- from _pytest.compat import ascii_escaped
- from _pytest.compat import assert_never
- from _pytest.compat import final
- from _pytest.compat import get_default_arg_names
- from _pytest.compat import get_real_func
- from _pytest.compat import getimfunc
- from _pytest.compat import getlocation
- from _pytest.compat import is_async_function
- 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 STRING_TYPES
- from _pytest.config import Config
- from _pytest.config import ExitCode
- from _pytest.config import hookimpl
- from _pytest.config.argparsing import Parser
- from _pytest.deprecated import check_ispytest
- from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
- from _pytest.deprecated import INSTANCE_COLLECTOR
- from _pytest.fixtures import FuncFixtureInfo
- from _pytest.main import Session
- from _pytest.mark import MARK_GEN
- from _pytest.mark import ParameterSet
- from _pytest.mark.structures import get_unpacked_marks
- from _pytest.mark.structures import Mark
- from _pytest.mark.structures import MarkDecorator
- from _pytest.mark.structures import normalize_mark_list
- from _pytest.outcomes import fail
- from _pytest.outcomes import skip
- from _pytest.pathlib import bestrelpath
- 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.scope import Scope
- from _pytest.warning_types import PytestCollectionWarning
- from _pytest.warning_types import PytestUnhandledCoroutineWarning
- if TYPE_CHECKING:
- from typing_extensions import Literal
- from _pytest.scope import _ScopeName
- _PYTEST_DIR = Path(_pytest.__file__).parent
- def pytest_addoption(parser: Parser) -> None:
- group = parser.getgroup("general")
- group.addoption(
- "--fixtures",
- "--funcargs",
- action="store_true",
- dest="showfixtures",
- default=False,
- help="show available fixtures, sorted by plugin appearance "
- "(fixtures with leading '_' are only shown with '-v')",
- )
- group.addoption(
- "--fixtures-per-test",
- action="store_true",
- dest="show_fixtures_per_test",
- default=False,
- help="show fixtures per test",
- )
- parser.addini(
- "python_files",
- type="args",
- # NOTE: default is also used in AssertionRewritingHook.
- default=["test_*.py", "*_test.py"],
- help="glob-style file patterns for Python test module discovery",
- )
- parser.addini(
- "python_classes",
- type="args",
- default=["Test"],
- help="prefixes or glob names for Python test class discovery",
- )
- parser.addini(
- "python_functions",
- type="args",
- default=["test"],
- help="prefixes or glob names for Python test function and method discovery",
- )
- parser.addini(
- "disable_test_id_escaping_and_forfeit_all_rights_to_community_support",
- type="bool",
- default=False,
- help="disable string escape non-ascii characters, might cause unwanted "
- "side effects(use at your own risk)",
- )
- def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
- if config.option.showfixtures:
- showfixtures(config)
- return 0
- if config.option.show_fixtures_per_test:
- show_fixtures_per_test(config)
- return 0
- return None
- def pytest_generate_tests(metafunc: "Metafunc") -> None:
- for marker in metafunc.definition.iter_markers(name="parametrize"):
- metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
- def pytest_configure(config: Config) -> None:
- config.addinivalue_line(
- "markers",
- "parametrize(argnames, argvalues): call a test function multiple "
- "times passing in different arguments in turn. argvalues generally "
- "needs to be a list of values if argnames specifies only one name "
- "or a list of tuples of values if argnames specifies multiple names. "
- "Example: @parametrize('arg1', [1,2]) would lead to two calls of the "
- "decorated test function, one with arg1=1 and another with arg1=2."
- "see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info "
- "and examples.",
- )
- config.addinivalue_line(
- "markers",
- "usefixtures(fixturename1, fixturename2, ...): mark tests as needing "
- "all of the specified fixtures. see "
- "https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures ",
- )
- def async_warn_and_skip(nodeid: str) -> None:
- msg = "async def functions are not natively supported and have been skipped.\n"
- msg += (
- "You need to install a suitable plugin for your async framework, for example:\n"
- )
- msg += " - anyio\n"
- msg += " - pytest-asyncio\n"
- msg += " - pytest-tornasync\n"
- msg += " - pytest-trio\n"
- msg += " - pytest-twisted"
- warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))
- skip(reason="async def function and no async plugin installed (see warnings)")
- @hookimpl(trylast=True)
- def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
- testfunction = pyfuncitem.obj
- if is_async_function(testfunction):
- async_warn_and_skip(pyfuncitem.nodeid)
- funcargs = pyfuncitem.funcargs
- testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
- result = testfunction(**testargs)
- if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
- async_warn_and_skip(pyfuncitem.nodeid)
- return True
- def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]:
- if file_path.suffix == ".py":
- if not parent.session.isinitpath(file_path):
- if not path_matches_patterns(
- file_path, parent.config.getini("python_files") + ["__init__.py"]
- ):
- return None
- ihook = parent.session.gethookproxy(file_path)
- module: Module = ihook.pytest_pycollect_makemodule(
- module_path=file_path, parent=parent
- )
- return module
- return None
- 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)
- def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
- if module_path.name == "__init__.py":
- pkg: Package = Package.from_parent(parent, path=module_path)
- return pkg
- mod: Module = Module.from_parent(parent, path=module_path)
- return mod
- @hookimpl(trylast=True)
- def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
- # Nothing was collected elsewhere, let's do it here.
- if safe_isclass(obj):
- if collector.istestclass(obj, name):
- return Class.from_parent(collector, name=name, obj=obj)
- elif collector.istestfunction(obj, name):
- # mock seems to store unbound methods (issue473), normalize it.
- obj = getattr(obj, "__func__", obj)
- # We need to try and unwrap the function if it's a functools.partial
- # or a functools.wrapped.
- # We mustn't if it's been wrapped with mock.patch (python 2 only).
- if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
- filename, lineno = getfslineno(obj)
- warnings.warn_explicit(
- message=PytestCollectionWarning(
- "cannot collect %r because it is not a function." % name
- ),
- category=None,
- filename=str(filename),
- lineno=lineno + 1,
- )
- elif getattr(obj, "__test__", True):
- if is_generator(obj):
- res = Function.from_parent(collector, name=name)
- reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
- name=name
- )
- res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
- res.warn(PytestCollectionWarning(reason))
- else:
- res = list(collector._genfunctions(name, obj))
- return res
- 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))
- 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__
- 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):
- # staticmethods 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. Since Python 3.6,
- # __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 = getattr(self.obj, "teardown_class", None)
- 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 this 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,))
- if setup_method is None and has_nose:
- setup_name = "setup"
- setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
- teardown_name = "teardown_method"
- teardown_method = getattr(self.obj, teardown_name, None)
- if teardown_method is None and has_nose:
- teardown_name = "teardown"
- teardown_method = getattr(self.obj, teardown_name, None)
- 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)
- yield
- if teardown_method is not None:
- func = getattr(self, teardown_name)
- _call_with_optional_argument(func, method)
- self.obj.__pytest_setup_method = xunit_setup_method_fixture
- class InstanceDummy:
- """Instance used to be a node type between Class and Function. It has been
- removed in pytest 7.0. Some plugins exist which reference `pytest.Instance`
- only to ignore it; this dummy class keeps them working. This will be removed
- in pytest 8."""
- pass
- # Note: module __getattr__ only works on Python>=3.7. Unfortunately
- # we can't provide this deprecation warning on Python 3.6.
- def __getattr__(name: str) -> object:
- if name == "Instance":
- warnings.warn(INSTANCE_COLLECTOR, 2)
- return InstanceDummy
- raise AttributeError(f"module {__name__} has no attribute {name}")
- def hasinit(obj: object) -> bool:
- 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
- @final
- @attr.s(frozen=True, slots=True, auto_attribs=True)
- class CallSpec2:
- """A planned parameterized invocation of a test function.
- Calculated during collection for a given test function's Metafunc.
- Once collection is over, each callspec is turned into a single Item
- and stored in item.callspec.
- """
- # arg name -> arg value which will be passed to the parametrized test
- # function (direct parameterization).
- funcargs: Dict[str, object] = attr.Factory(dict)
- # arg name -> arg value which will be passed to a fixture of the same name
- # (indirect parametrization).
- params: Dict[str, object] = attr.Factory(dict)
- # arg name -> arg index.
- indices: Dict[str, int] = attr.Factory(dict)
- # Used for sorting parametrized resources.
- _arg2scope: Dict[str, Scope] = attr.Factory(dict)
- # Parts which will be added to the item's name in `[..]` separated by "-".
- _idlist: List[str] = attr.Factory(list)
- # Marks which will be applied to the item.
- marks: List[Mark] = attr.Factory(list)
- def setmulti(
- self,
- *,
- valtypes: Mapping[str, "Literal['params', 'funcargs']"],
- argnames: Iterable[str],
- valset: Iterable[object],
- id: str,
- marks: Iterable[Union[Mark, MarkDecorator]],
- scope: Scope,
- param_index: int,
- ) -> "CallSpec2":
- funcargs = self.funcargs.copy()
- params = self.params.copy()
- indices = self.indices.copy()
- arg2scope = self._arg2scope.copy()
- for arg, val in zip(argnames, valset):
- if arg in params or arg in funcargs:
- raise ValueError(f"duplicate {arg!r}")
- valtype_for_arg = valtypes[arg]
- if valtype_for_arg == "params":
- params[arg] = val
- elif valtype_for_arg == "funcargs":
- funcargs[arg] = val
- else:
- assert_never(valtype_for_arg)
- indices[arg] = param_index
- arg2scope[arg] = scope
- return CallSpec2(
- funcargs=funcargs,
- params=params,
- arg2scope=arg2scope,
- indices=indices,
- idlist=[*self._idlist, id],
- marks=[*self.marks, *normalize_mark_list(marks)],
- )
- def getparam(self, name: str) -> object:
- try:
- return self.params[name]
- except KeyError as e:
- raise ValueError(name) from e
- @property
- def id(self) -> str:
- return "-".join(self._idlist)
- @final
- class Metafunc:
- """Objects passed to the :hook:`pytest_generate_tests` hook.
- They help to inspect a test function and to generate tests according to
- test configuration or values specified in the class or module where a
- test function is defined.
- """
- def __init__(
- self,
- definition: "FunctionDefinition",
- fixtureinfo: fixtures.FuncFixtureInfo,
- config: Config,
- cls=None,
- module=None,
- *,
- _ispytest: bool = False,
- ) -> None:
- check_ispytest(_ispytest)
- #: Access to the underlying :class:`_pytest.python.FunctionDefinition`.
- self.definition = definition
- #: Access to the :class:`pytest.Config` object for the test session.
- self.config = config
- #: The module object where the test function is defined in.
- self.module = module
- #: Underlying Python test function.
- self.function = definition.obj
- #: Set of fixture names required by the test function.
- self.fixturenames = fixtureinfo.names_closure
- #: Class object where the test function is defined in or ``None``.
- self.cls = cls
- self._arg2fixturedefs = fixtureinfo.name2fixturedefs
- # Result of parametrize().
- self._calls: List[CallSpec2] = []
- def parametrize(
- self,
- argnames: Union[str, List[str], Tuple[str, ...]],
- argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
- indirect: Union[bool, Sequence[str]] = False,
- ids: Optional[
- Union[
- Iterable[Union[None, str, float, int, bool]],
- Callable[[Any], Optional[object]],
- ]
- ] = None,
- scope: "Optional[_ScopeName]" = None,
- *,
- _param_mark: Optional[Mark] = None,
- ) -> None:
- """Add new invocations to the underlying test function using the list
- of argvalues for the given argnames. Parametrization is performed
- during the collection phase. If you need to setup expensive resources
- see about setting indirect to do it rather than at test setup time.
- Can be called multiple times, in which case each call parametrizes all
- previous parametrizations, e.g.
- ::
- unparametrized: t
- parametrize ["x", "y"]: t[x], t[y]
- parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2]
- :param argnames:
- A comma-separated string denoting one or more argument names, or
- a list/tuple of argument strings.
- :param argvalues:
- The list of argvalues determines how often a test is invoked with
- different argument values.
- If only one argname was specified argvalues is a list of values.
- If N argnames were specified, argvalues must be a list of
- N-tuples, where each tuple-element specifies a value for its
- respective argname.
- :param indirect:
- A list of arguments' names (subset of argnames) or a boolean.
- If True the list contains all names from the argnames. Each
- argvalue corresponding to an argname in this list will
- be passed as request.param to its respective argname fixture
- function so that it can perform more expensive setups during the
- setup phase of a test rather than at collection time.
- :param ids:
- Sequence of (or generator for) ids for ``argvalues``,
- or a callable to return part of the id for each argvalue.
- With sequences (and generators like ``itertools.count()``) the
- returned ids should be of type ``string``, ``int``, ``float``,
- ``bool``, or ``None``.
- They are mapped to the corresponding index in ``argvalues``.
- ``None`` means to use the auto-generated id.
- If it is a callable it will be called for each entry in
- ``argvalues``, and the return value is used as part of the
- auto-generated id for the whole set (where parts are joined with
- dashes ("-")).
- This is useful to provide more specific ids for certain items, e.g.
- dates. Returning ``None`` will use an auto-generated id.
- If no ids are provided they will be generated automatically from
- the argvalues.
- :param scope:
- If specified it denotes the scope of the parameters.
- The scope is used for grouping tests by parameter instances.
- It will also override any fixture-function defined scope, allowing
- to set a dynamic scope using test context or configuration.
- """
- argnames, parameters = ParameterSet._for_parametrize(
- argnames,
- argvalues,
- self.function,
- self.config,
- nodeid=self.definition.nodeid,
- )
- del argvalues
- if "request" in argnames:
- fail(
- "'request' is a reserved name and cannot be used in @pytest.mark.parametrize",
- pytrace=False,
- )
- if scope is not None:
- scope_ = Scope.from_user(
- scope, descr=f"parametrize() call in {self.function.__name__}"
- )
- else:
- scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
- self._validate_if_using_arg_names(argnames, indirect)
- arg_values_types = self._resolve_arg_value_types(argnames, indirect)
- # Use any already (possibly) generated ids with parametrize Marks.
- if _param_mark and _param_mark._param_ids_from:
- generated_ids = _param_mark._param_ids_from._param_ids_generated
- if generated_ids is not None:
- ids = generated_ids
- ids = self._resolve_arg_ids(
- argnames, ids, parameters, nodeid=self.definition.nodeid
- )
- # Store used (possibly generated) ids with parametrize Marks.
- if _param_mark and _param_mark._param_ids_from and generated_ids is None:
- object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
- # Create the new calls: if we are parametrize() multiple times (by applying the decorator
- # more than once) then we accumulate those calls generating the cartesian product
- # of all calls.
- newcalls = []
- for callspec in self._calls or [CallSpec2()]:
- for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
- newcallspec = callspec.setmulti(
- valtypes=arg_values_types,
- argnames=argnames,
- valset=param_set.values,
- id=param_id,
- marks=param_set.marks,
- scope=scope_,
- param_index=param_index,
- )
- newcalls.append(newcallspec)
- self._calls = newcalls
- def _resolve_arg_ids(
- self,
- argnames: Sequence[str],
- ids: Optional[
- Union[
- Iterable[Union[None, str, float, int, bool]],
- Callable[[Any], Optional[object]],
- ]
- ],
- parameters: Sequence[ParameterSet],
- nodeid: str,
- ) -> List[str]:
- """Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
- to ``parametrize``.
- :param List[str] argnames: List of argument names passed to ``parametrize()``.
- :param ids: The ids parameter of the parametrized call (see docs).
- :param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``.
- :param str str: The nodeid of the item that generated this parametrized call.
- :rtype: List[str]
- :returns: The list of ids for each argname given.
- """
- if ids is None:
- idfn = None
- ids_ = None
- elif callable(ids):
- idfn = ids
- ids_ = None
- else:
- idfn = None
- ids_ = self._validate_ids(ids, parameters, self.function.__name__)
- return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid)
- def _validate_ids(
- self,
- ids: Iterable[Union[None, str, float, int, bool]],
- parameters: Sequence[ParameterSet],
- func_name: str,
- ) -> List[Union[None, str]]:
- try:
- num_ids = len(ids) # type: ignore[arg-type]
- except TypeError:
- try:
- iter(ids)
- except TypeError as e:
- raise TypeError("ids must be a callable or an iterable") from e
- num_ids = len(parameters)
- # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
- if num_ids != len(parameters) and num_ids != 0:
- msg = "In {}: {} parameter sets specified, with different number of ids: {}"
- fail(msg.format(func_name, len(parameters), num_ids), pytrace=False)
- new_ids = []
- for idx, id_value in enumerate(itertools.islice(ids, num_ids)):
- if id_value is None or isinstance(id_value, str):
- new_ids.append(id_value)
- elif isinstance(id_value, (float, int, bool)):
- new_ids.append(str(id_value))
- else:
- msg = ( # type: ignore[unreachable]
- "In {}: ids must be list of string/float/int/bool, "
- "found: {} (type: {!r}) at index {}"
- )
- fail(
- msg.format(func_name, saferepr(id_value), type(id_value), idx),
- pytrace=False,
- )
- return new_ids
- def _resolve_arg_value_types(
- self,
- argnames: Sequence[str],
- indirect: Union[bool, Sequence[str]],
- ) -> Dict[str, "Literal['params', 'funcargs']"]:
- """Resolve if each parametrized argument must be considered a
- parameter to a fixture or a "funcarg" to the function, based on the
- ``indirect`` parameter of the parametrized() call.
- :param List[str] argnames: List of argument names passed to ``parametrize()``.
- :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
- :rtype: Dict[str, str]
- A dict mapping each arg name to either:
- * "params" if the argname should be the parameter of a fixture of the same name.
- * "funcargs" if the argname should be a parameter to the parametrized test function.
- """
- if isinstance(indirect, bool):
- valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys(
- argnames, "params" if indirect else "funcargs"
- )
- elif isinstance(indirect, Sequence):
- valtypes = dict.fromkeys(argnames, "funcargs")
- for arg in indirect:
- if arg not in argnames:
- fail(
- "In {}: indirect fixture '{}' doesn't exist".format(
- self.function.__name__, arg
- ),
- pytrace=False,
- )
- valtypes[arg] = "params"
- else:
- fail(
- "In {func}: expected Sequence or boolean for indirect, got {type}".format(
- type=type(indirect).__name__, func=self.function.__name__
- ),
- pytrace=False,
- )
- return valtypes
- def _validate_if_using_arg_names(
- self,
- argnames: Sequence[str],
- indirect: Union[bool, Sequence[str]],
- ) -> None:
- """Check if all argnames are being used, by default values, or directly/indirectly.
- :param List[str] argnames: List of argument names passed to ``parametrize()``.
- :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
- :raises ValueError: If validation fails.
- """
- default_arg_names = set(get_default_arg_names(self.function))
- func_name = self.function.__name__
- for arg in argnames:
- if arg not in self.fixturenames:
- if arg in default_arg_names:
- fail(
- "In {}: function already takes an argument '{}' with a default value".format(
- func_name, arg
- ),
- pytrace=False,
- )
- else:
- if isinstance(indirect, Sequence):
- name = "fixture" if arg in indirect else "argument"
- else:
- name = "fixture" if indirect else "argument"
- fail(
- f"In {func_name}: function uses no {name} '{arg}'",
- pytrace=False,
- )
- def _find_parametrized_scope(
- argnames: Sequence[str],
- arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
- indirect: Union[bool, Sequence[str]],
- ) -> Scope:
- """Find the most appropriate scope for a parametrized call based on its arguments.
- When there's at least one direct argument, always use "function" scope.
- When a test function is parametrized and all its arguments are indirect
- (e.g. fixtures), return the most narrow scope based on the fixtures used.
- Related to issue #1832, based on code posted by @Kingdread.
- """
- if isinstance(indirect, Sequence):
- all_arguments_are_fixtures = len(indirect) == len(argnames)
- else:
- all_arguments_are_fixtures = bool(indirect)
- if all_arguments_are_fixtures:
- fixturedefs = arg2fixturedefs or {}
- used_scopes = [
- fixturedef[0]._scope
- for name, fixturedef in fixturedefs.items()
- if name in argnames
- ]
- # Takes the most narrow scope from used fixtures.
- return min(used_scopes, default=Scope.Function)
- return Scope.Function
- def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str:
- if config is None:
- escape_option = False
- else:
- escape_option = config.getini(
- "disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
- )
- # TODO: If escaping is turned off and the user passes bytes,
- # will return a bytes. For now we ignore this but the
- # code *probably* doesn't handle this case.
- return val if escape_option else ascii_escaped(val) # type: ignore
- def _idval(
- val: object,
- argname: str,
- idx: int,
- idfn: Optional[Callable[[Any], Optional[object]]],
- nodeid: Optional[str],
- config: Optional[Config],
- ) -> str:
- if idfn:
- try:
- generated_id = idfn(val)
- if generated_id is not None:
- val = generated_id
- except Exception as e:
- prefix = f"{nodeid}: " if nodeid is not None else ""
- msg = "error raised while trying to determine id of parameter '{}' at position {}"
- msg = prefix + msg.format(argname, idx)
- raise ValueError(msg) from e
- elif config:
- hook_id: Optional[str] = config.hook.pytest_make_parametrize_id(
- config=config, val=val, argname=argname
- )
- if hook_id:
- return hook_id
- if isinstance(val, STRING_TYPES):
- return _ascii_escaped_by_config(val, config)
- elif val is None or isinstance(val, (float, int, bool, complex)):
- return str(val)
- elif isinstance(val, Pattern):
- return ascii_escaped(val.pattern)
- elif val is NOTSET:
- # Fallback to default. Note that NOTSET is an enum.Enum.
- pass
- elif isinstance(val, enum.Enum):
- return str(val)
- elif isinstance(getattr(val, "__name__", None), str):
- # Name of a class, function, module, etc.
- name: str = getattr(val, "__name__")
- return name
- return str(argname) + str(idx)
- def _idvalset(
- idx: int,
- parameterset: ParameterSet,
- argnames: Iterable[str],
- idfn: Optional[Callable[[Any], Optional[object]]],
- ids: Optional[List[Union[None, str]]],
- nodeid: Optional[str],
- config: Optional[Config],
- ) -> str:
- if parameterset.id is not None:
- return parameterset.id
- id = None if ids is None or idx >= len(ids) else ids[idx]
- if id is None:
- this_id = [
- _idval(val, argname, idx, idfn, nodeid=nodeid, config=config)
- for val, argname in zip(parameterset.values, argnames)
- ]
- return "-".join(this_id)
- else:
- return _ascii_escaped_by_config(id, config)
- def idmaker(
- argnames: Iterable[str],
- parametersets: Iterable[ParameterSet],
- idfn: Optional[Callable[[Any], Optional[object]]] = None,
- ids: Optional[List[Union[None, str]]] = None,
- config: Optional[Config] = None,
- nodeid: Optional[str] = None,
- ) -> List[str]:
- resolved_ids = [
- _idvalset(
- valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid
- )
- for valindex, parameterset in enumerate(parametersets)
- ]
- # All IDs must be unique!
- unique_ids = set(resolved_ids)
- if len(unique_ids) != len(resolved_ids):
- # Record the number of occurrences of each test ID.
- test_id_counts = Counter(resolved_ids)
- # Map the test ID to its next suffix.
- test_id_suffixes: Dict[str, int] = defaultdict(int)
- # Suffix non-unique IDs to make them unique.
- for index, test_id in enumerate(resolved_ids):
- if test_id_counts[test_id] > 1:
- resolved_ids[index] = f"{test_id}{test_id_suffixes[test_id]}"
- test_id_suffixes[test_id] += 1
- return resolved_ids
- def _pretty_fixture_path(func) -> str:
- cwd = Path.cwd()
- loc = Path(getlocation(func, str(cwd)))
- prefix = Path("...", "_pytest")
- try:
- return str(prefix / loc.relative_to(_PYTEST_DIR))
- except ValueError:
- return bestrelpath(cwd, loc)
- def show_fixtures_per_test(config):
- from _pytest.main import wrap_session
- return wrap_session(config, _show_fixtures_per_test)
- def _show_fixtures_per_test(config: Config, session: Session) -> None:
- import _pytest.config
- session.perform_collect()
- curdir = Path.cwd()
- tw = _pytest.config.create_terminal_writer(config)
- verbose = config.getvalue("verbose")
- def get_best_relpath(func) -> str:
- loc = getlocation(func, str(curdir))
- return bestrelpath(curdir, Path(loc))
- def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
- argname = fixture_def.argname
- if verbose <= 0 and argname.startswith("_"):
- return
- prettypath = _pretty_fixture_path(fixture_def.func)
- tw.write(f"{argname}", green=True)
- tw.write(f" -- {prettypath}", yellow=True)
- tw.write("\n")
- fixture_doc = inspect.getdoc(fixture_def.func)
- if fixture_doc:
- write_docstring(
- tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
- )
- else:
- tw.line(" no docstring available", red=True)
- def write_item(item: nodes.Item) -> None:
- # Not all items have _fixtureinfo attribute.
- info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
- if info is None or not info.name2fixturedefs:
- # This test item does not use any fixtures.
- return
- tw.line()
- tw.sep("-", f"fixtures used by {item.name}")
- # TODO: Fix this type ignore.
- tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
- # dict key not used in loop but needed for sorting.
- for _, fixturedefs in sorted(info.name2fixturedefs.items()):
- assert fixturedefs is not None
- if not fixturedefs:
- continue
- # Last item is expected to be the one used by the test item.
- write_fixture(fixturedefs[-1])
- for session_item in session.items:
- write_item(session_item)
- def showfixtures(config: Config) -> Union[int, ExitCode]:
- from _pytest.main import wrap_session
- return wrap_session(config, _showfixtures_main)
- def _showfixtures_main(config: Config, session: Session) -> None:
- import _pytest.config
- session.perform_collect()
- curdir = Path.cwd()
- tw = _pytest.config.create_terminal_writer(config)
- verbose = config.getvalue("verbose")
- fm = session._fixturemanager
- available = []
- seen: Set[Tuple[str, str]] = set()
- for argname, fixturedefs in fm._arg2fixturedefs.items():
- assert fixturedefs is not None
- if not fixturedefs:
- continue
- for fixturedef in fixturedefs:
- loc = getlocation(fixturedef.func, str(curdir))
- if (fixturedef.argname, loc) in seen:
- continue
- seen.add((fixturedef.argname, loc))
- available.append(
- (
- len(fixturedef.baseid),
- fixturedef.func.__module__,
- _pretty_fixture_path(fixturedef.func),
- fixturedef.argname,
- fixturedef,
- )
- )
- available.sort()
- currentmodule = None
- for baseid, module, prettypath, argname, fixturedef in available:
- if currentmodule != module:
- if not module.startswith("_pytest."):
- tw.line()
- tw.sep("-", f"fixtures defined from {module}")
- currentmodule = module
- if verbose <= 0 and argname.startswith("_"):
- continue
- tw.write(f"{argname}", green=True)
- if fixturedef.scope != "function":
- tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
- tw.write(f" -- {prettypath}", yellow=True)
- tw.write("\n")
- doc = inspect.getdoc(fixturedef.func)
- if doc:
- write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
- else:
- tw.line(" no docstring available", red=True)
- tw.line()
- def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
- for line in doc.split("\n"):
- tw.line(indent + line)
- 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=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.keywords.update(self.obj.__dict__)
- self.own_markers.extend(get_unpacked_marks(self.obj))
- if callspec:
- self.callspec = callspec
- # this is total hostile and a mess
- # keywords are broken by design by now
- # this will be redeemed later
- for mark in callspec.marks:
- # feel free to cry, this was broken for years before
- # and keywords can't fix it per design
- self.keywords[mark.name] = mark
- self.own_markers.extend(normalize_mark_list(callspec.marks))
- if keywords:
- self.keywords.update(keywords)
- # todo: this is a hell of a hack
- # https://github.com/pytest-dev/pytest/issues/4569
- self.keywords.update(
- {
- mark.name: True
- for mark in self.iter_markers()
- if mark.name not in self.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
|