12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856 |
- """Python test discovery, setup and run of test functions."""
- import dataclasses
- 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 _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._code.code import Traceback
- 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 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 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 PytestReturnNotNoneWarning
- 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)
- elif result is not None:
- warnings.warn(
- PytestReturnNotNoneWarning(
- f"Expected None, but {pyfuncitem.nodeid} returned {result!r}, which will be an error in a "
- "future version of pytest. Did you mean to use `assert` instead of `return`?"
- )
- )
- 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: Union["Module", "Class"], name: str, obj: object
- ) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]:
- assert isinstance(collector, (Class, Module)), type(collector)
- # Nothing was collected elsewhere, let's do it here.
- if safe_isclass(obj):
- if collector.istestclass(obj, name):
- klass: Class = Class.from_parent(collector, name=name, obj=obj)
- return klass
- 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 = 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))
- return res
- else:
- return list(collector._genfunctions(name, obj))
- 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 in a Python module."""
- 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):
- """Collector for files and directories in a Python packages -- directories
- with an `__init__.py` file."""
- 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,
- ) -> 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 _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
- 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
- # Always collect the __init__ first.
- if path_matches_patterns(self.path, self.config.getini("python_files")):
- yield Module.from_parent(self, path=self.path)
- 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 (and nested classes) in a Python class."""
- @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:
- """Instance used to be a node type between Class and Function. It has been
- removed in pytest 7.0. Some plugins exist which reference `pytest.Instance`
- only to ignore it; this dummy class keeps them working. This will be removed
- in pytest 8."""
- def __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
- @dataclasses.dataclass(frozen=True)
- class IdMaker:
- """Make IDs for a parametrization."""
- __slots__ = (
- "argnames",
- "parametersets",
- "idfn",
- "ids",
- "config",
- "nodeid",
- "func_name",
- )
- # The argnames of the parametrization.
- argnames: Sequence[str]
- # The ParameterSets of the parametrization.
- parametersets: Sequence[ParameterSet]
- # Optionally, a user-provided callable to make IDs for parameters in a
- # ParameterSet.
- idfn: Optional[Callable[[Any], Optional[object]]]
- # Optionally, explicit IDs for ParameterSets by index.
- ids: Optional[Sequence[Optional[object]]]
- # Optionally, the pytest config.
- # Used for controlling ASCII escaping, and for calling the
- # :hook:`pytest_make_parametrize_id` hook.
- config: Optional[Config]
- # Optionally, the ID of the node being parametrized.
- # Used only for clearer error messages.
- nodeid: Optional[str]
- # Optionally, the ID of the function being parametrized.
- # Used only for clearer error messages.
- func_name: Optional[str]
- def make_unique_parameterset_ids(self) -> List[str]:
- """Make a unique identifier for each ParameterSet, that may be used to
- identify the parametrization in a node ID.
- Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is
- - user-provided id, if given
- - else an id derived from the value, applicable for certain types
- - else <argname><parameterset index>
- The counter suffix is appended only in case a string wouldn't be unique
- otherwise.
- """
- resolved_ids = list(self._limit_ids(self._resolve_ids(), limit=500))
- # All IDs must be unique!
- if len(resolved_ids) != len(set(resolved_ids)):
- # Record the number of occurrences of each ID.
- id_counts = Counter(resolved_ids)
- # Map the ID to its next suffix.
- id_suffixes: Dict[str, int] = defaultdict(int)
- # Suffix non-unique IDs to make them unique.
- for index, id in enumerate(resolved_ids):
- if id_counts[id] > 1:
- resolved_ids[index] = f"{id}{id_suffixes[id]}"
- id_suffixes[id] += 1
- return resolved_ids
- def _limit_ids(self, ids, limit=500):
- prefix_count = {}
- limit -= 6
- assert limit > 0
- for idval in ids:
- if len(idval) > limit:
- prefix = idval[:limit]
- idx = prefix_count.get(prefix, -1) + 1
- prefix_count[prefix] = idx
- idval = "{}-{}".format(prefix, idx)
- yield idval
- def _resolve_ids(self) -> Iterable[str]:
- """Resolve IDs for all ParameterSets (may contain duplicates)."""
- for idx, parameterset in enumerate(self.parametersets):
- if parameterset.id is not None:
- # ID provided directly - pytest.param(..., id="...")
- yield parameterset.id
- elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
- # ID provided in the IDs list - parametrize(..., ids=[...]).
- yield self._idval_from_value_required(self.ids[idx], idx)
- else:
- # ID not provided - generate it.
- yield "-".join(
- self._idval(val, argname, idx)
- for val, argname in zip(parameterset.values, self.argnames)
- )
- def _idval(self, val: object, argname: str, idx: int) -> str:
- """Make an ID for a parameter in a ParameterSet."""
- idval = self._idval_from_function(val, argname, idx)
- if idval is not None:
- return idval
- idval = self._idval_from_hook(val, argname)
- if idval is not None:
- return idval
- idval = self._idval_from_value(val)
- if idval is not None:
- return idval
- return self._idval_from_argname(argname, idx)
- def _idval_from_function(
- self, val: object, argname: str, idx: int
- ) -> Optional[str]:
- """Try to make an ID for a parameter in a ParameterSet using the
- user-provided id callable, if given."""
- if self.idfn is None:
- return None
- try:
- id = self.idfn(val)
- except Exception as e:
- prefix = f"{self.nodeid}: " if self.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
- if id is None:
- return None
- return self._idval_from_value(id)
- def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
- """Try to make an ID for a parameter in a ParameterSet by calling the
- :hook:`pytest_make_parametrize_id` hook."""
- if self.config:
- id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
- config=self.config, val=val, argname=argname
- )
- return id
- return None
- def _idval_from_value(self, val: object) -> Optional[str]:
- """Try to make an ID for a parameter in a ParameterSet from its value,
- if the value type is supported."""
- if isinstance(val, STRING_TYPES):
- return _ascii_escaped_by_config(val, self.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 None
- def _idval_from_value_required(self, val: object, idx: int) -> str:
- """Like _idval_from_value(), but fails if the type is not supported."""
- id = self._idval_from_value(val)
- if id is not None:
- return id
- # Fail.
- if self.func_name is not None:
- prefix = f"In {self.func_name}: "
- elif self.nodeid is not None:
- prefix = f"In {self.nodeid}: "
- else:
- prefix = ""
- msg = (
- f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
- "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
- )
- fail(msg, pytrace=False)
- @staticmethod
- def _idval_from_argname(argname: str, idx: int) -> str:
- """Make an ID for a parameter in a ParameterSet from the argument name
- and the index of the ParameterSet."""
- return str(argname) + str(idx)
- @final
- @dataclasses.dataclass(frozen=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] = dataclasses.field(default_factory=dict)
- # arg name -> arg value which will be passed to a fixture of the same name
- # (indirect parametrization).
- params: Dict[str, object] = dataclasses.field(default_factory=dict)
- # arg name -> arg index.
- indices: Dict[str, int] = dataclasses.field(default_factory=dict)
- # Used for sorting parametrized resources.
- _arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
- # Parts which will be added to the item's name in `[..]` separated by "-".
- _idlist: List[str] = dataclasses.field(default_factory=list)
- # Marks which will be applied to the item.
- marks: List[Mark] = dataclasses.field(default_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 parametrization of {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,
- indices=indices,
- _arg2scope=arg2scope,
- _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, Sequence[str]],
- argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
- indirect: Union[bool, Sequence[str]] = False,
- ids: Optional[
- Union[Iterable[Optional[object]], 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 per test function (but only on different
- argument names), 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, parametersets = 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_parameter_set_ids(
- argnames, ids, parametersets, 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, parametersets)
- ):
- 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_parameter_set_ids(
- self,
- argnames: Sequence[str],
- ids: Optional[
- Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
- ],
- parametersets: Sequence[ParameterSet],
- nodeid: str,
- ) -> List[str]:
- """Resolve the actual ids for the given parameter sets.
- :param argnames:
- Argument names passed to ``parametrize()``.
- :param ids:
- The `ids` parameter of the ``parametrize()`` call (see docs).
- :param parametersets:
- The parameter sets, each containing a set of values corresponding
- to ``argnames``.
- :param nodeid str:
- The nodeid of the definition item that generated this
- parametrization.
- :returns:
- List with ids for each parameter set given.
- """
- if ids is None:
- idfn = None
- ids_ = None
- elif callable(ids):
- idfn = ids
- ids_ = None
- else:
- idfn = None
- ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
- id_maker = IdMaker(
- argnames,
- parametersets,
- idfn,
- ids_,
- self.config,
- nodeid=nodeid,
- func_name=self.function.__name__,
- )
- return id_maker.make_unique_parameterset_ids()
- def _validate_ids(
- self,
- ids: Iterable[Optional[object]],
- parametersets: Sequence[ParameterSet],
- func_name: str,
- ) -> List[Optional[object]]:
- 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(parametersets)
- # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
- if num_ids != len(parametersets) and num_ids != 0:
- msg = "In {}: {} parameter sets specified, with different number of ids: {}"
- fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
- return list(itertools.islice(ids, num_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 _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):
- """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 _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
- 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
- ntraceback = ntraceback.filter(excinfo)
- # 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(ntraceback) > 2:
- ntraceback = Traceback(
- entry
- if i == 0 or i == len(ntraceback) - 1
- else entry.with_repr_style("short")
- for i, entry in enumerate(ntraceback)
- )
- return ntraceback
- return excinfo.traceback
- # 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 stop 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
|