Merge pull request #6836 from bluetech/store
Add a typing-compatible mechanism for ad-hoc attributes on various objects
This commit is contained in:
commit
92767fec51
|
@ -42,6 +42,7 @@ from _pytest.compat import TYPE_CHECKING
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.pathlib import Path
|
from _pytest.pathlib import Path
|
||||||
|
from _pytest.store import Store
|
||||||
from _pytest.warning_types import PytestConfigWarning
|
from _pytest.warning_types import PytestConfigWarning
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -791,6 +792,9 @@ class Config:
|
||||||
self._override_ini = () # type: Sequence[str]
|
self._override_ini = () # type: Sequence[str]
|
||||||
self._opt2dest = {} # type: Dict[str, str]
|
self._opt2dest = {} # type: Dict[str, str]
|
||||||
self._cleanup = [] # type: List[Callable[[], None]]
|
self._cleanup = [] # type: List[Callable[[], None]]
|
||||||
|
# A place where plugins can store information on the config for their
|
||||||
|
# own use. Currently only intended for internal plugins.
|
||||||
|
self._store = Store()
|
||||||
self.pluginmanager.register(self, "pytestconfig")
|
self.pluginmanager.register(self, "pytestconfig")
|
||||||
self._configured = False
|
self._configured = False
|
||||||
self.hook.pytest_addoption.call_historic(
|
self.hook.pytest_addoption.call_historic(
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.store import StoreKey
|
||||||
|
|
||||||
|
|
||||||
|
fault_handler_stderr_key = StoreKey[TextIO]()
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
|
@ -46,8 +51,8 @@ class FaultHandlerHooks:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
stderr_fd_copy = os.dup(self._get_stderr_fileno())
|
stderr_fd_copy = os.dup(self._get_stderr_fileno())
|
||||||
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
|
config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
||||||
faulthandler.enable(file=config.fault_handler_stderr)
|
faulthandler.enable(file=config._store[fault_handler_stderr_key])
|
||||||
|
|
||||||
def pytest_unconfigure(self, config):
|
def pytest_unconfigure(self, config):
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
@ -57,8 +62,8 @@ class FaultHandlerHooks:
|
||||||
# re-enable the faulthandler, attaching it to the default sys.stderr
|
# re-enable the faulthandler, attaching it to the default sys.stderr
|
||||||
# so we can see crashes after pytest has finished, usually during
|
# so we can see crashes after pytest has finished, usually during
|
||||||
# garbage collection during interpreter shutdown
|
# garbage collection during interpreter shutdown
|
||||||
config.fault_handler_stderr.close()
|
config._store[fault_handler_stderr_key].close()
|
||||||
del config.fault_handler_stderr
|
del config._store[fault_handler_stderr_key]
|
||||||
faulthandler.enable(file=self._get_stderr_fileno())
|
faulthandler.enable(file=self._get_stderr_fileno())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -78,7 +83,7 @@ class FaultHandlerHooks:
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtest_protocol(self, item):
|
def pytest_runtest_protocol(self, item):
|
||||||
timeout = self.get_timeout_config_value(item.config)
|
timeout = self.get_timeout_config_value(item.config)
|
||||||
stderr = item.config.fault_handler_stderr
|
stderr = item.config._store[fault_handler_stderr_key]
|
||||||
if timeout > 0 and stderr is not None:
|
if timeout > 0 and stderr is not None:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,13 @@ import pytest
|
||||||
from _pytest import deprecated
|
from _pytest import deprecated
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest.config import filename_arg
|
from _pytest.config import filename_arg
|
||||||
|
from _pytest.store import StoreKey
|
||||||
from _pytest.warnings import _issue_warning_captured
|
from _pytest.warnings import _issue_warning_captured
|
||||||
|
|
||||||
|
|
||||||
|
xml_key = StoreKey["LogXML"]()
|
||||||
|
|
||||||
|
|
||||||
class Junit(py.xml.Namespace):
|
class Junit(py.xml.Namespace):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -260,7 +264,7 @@ def _warn_incompatibility_with_xunit2(request, fixture_name):
|
||||||
"""Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions"""
|
"""Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions"""
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
xml = getattr(request.config, "_xml", None)
|
xml = request.config._store.get(xml_key, None)
|
||||||
if xml is not None and xml.family not in ("xunit1", "legacy"):
|
if xml is not None and xml.family not in ("xunit1", "legacy"):
|
||||||
request.node.warn(
|
request.node.warn(
|
||||||
PytestWarning(
|
PytestWarning(
|
||||||
|
@ -312,7 +316,7 @@ def record_xml_attribute(request):
|
||||||
|
|
||||||
attr_func = add_attr_noop
|
attr_func = add_attr_noop
|
||||||
|
|
||||||
xml = getattr(request.config, "_xml", None)
|
xml = request.config._store.get(xml_key, None)
|
||||||
if xml is not None:
|
if xml is not None:
|
||||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||||
attr_func = node_reporter.add_attribute
|
attr_func = node_reporter.add_attribute
|
||||||
|
@ -353,7 +357,7 @@ def record_testsuite_property(request):
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
_check_record_param_type("name", name)
|
_check_record_param_type("name", name)
|
||||||
|
|
||||||
xml = getattr(request.config, "_xml", None)
|
xml = request.config._store.get(xml_key, None)
|
||||||
if xml is not None:
|
if xml is not None:
|
||||||
record_func = xml.add_global_property # noqa
|
record_func = xml.add_global_property # noqa
|
||||||
return record_func
|
return record_func
|
||||||
|
@ -412,7 +416,7 @@ def pytest_configure(config):
|
||||||
if not junit_family:
|
if not junit_family:
|
||||||
_issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2)
|
_issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2)
|
||||||
junit_family = "xunit1"
|
junit_family = "xunit1"
|
||||||
config._xml = LogXML(
|
config._store[xml_key] = LogXML(
|
||||||
xmlpath,
|
xmlpath,
|
||||||
config.option.junitprefix,
|
config.option.junitprefix,
|
||||||
config.getini("junit_suite_name"),
|
config.getini("junit_suite_name"),
|
||||||
|
@ -421,13 +425,13 @@ def pytest_configure(config):
|
||||||
junit_family,
|
junit_family,
|
||||||
config.getini("junit_log_passing_tests"),
|
config.getini("junit_log_passing_tests"),
|
||||||
)
|
)
|
||||||
config.pluginmanager.register(config._xml)
|
config.pluginmanager.register(config._store[xml_key])
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
def pytest_unconfigure(config):
|
||||||
xml = getattr(config, "_xml", None)
|
xml = config._store.get(xml_key, None)
|
||||||
if xml:
|
if xml:
|
||||||
del config._xml
|
del config._store[xml_key]
|
||||||
config.pluginmanager.unregister(xml)
|
config.pluginmanager.unregister(xml)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
""" generic mechanism for marking and selecting python functions. """
|
""" generic mechanism for marking and selecting python functions. """
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .legacy import matchkeyword
|
from .legacy import matchkeyword
|
||||||
from .legacy import matchmark
|
from .legacy import matchmark
|
||||||
from .structures import EMPTY_PARAMETERSET_OPTION
|
from .structures import EMPTY_PARAMETERSET_OPTION
|
||||||
|
@ -8,12 +10,17 @@ from .structures import MARK_GEN
|
||||||
from .structures import MarkDecorator
|
from .structures import MarkDecorator
|
||||||
from .structures import MarkGenerator
|
from .structures import MarkGenerator
|
||||||
from .structures import ParameterSet
|
from .structures import ParameterSet
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config import UsageError
|
from _pytest.config import UsageError
|
||||||
|
from _pytest.store import StoreKey
|
||||||
|
|
||||||
__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
|
__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
|
||||||
|
|
||||||
|
|
||||||
|
old_mark_config_key = StoreKey[Optional[Config]]()
|
||||||
|
|
||||||
|
|
||||||
def param(*values, **kw):
|
def param(*values, **kw):
|
||||||
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
|
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
|
||||||
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
|
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
|
||||||
|
@ -145,7 +152,7 @@ def pytest_collection_modifyitems(items, config):
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
config._old_mark_config = MARK_GEN._config
|
config._store[old_mark_config_key] = MARK_GEN._config
|
||||||
MARK_GEN._config = config
|
MARK_GEN._config = config
|
||||||
|
|
||||||
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||||
|
@ -158,4 +165,4 @@ def pytest_configure(config):
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
def pytest_unconfigure(config):
|
||||||
MARK_GEN._config = getattr(config, "_old_mark_config", None)
|
MARK_GEN._config = config._store.get(old_mark_config_key, None)
|
||||||
|
|
|
@ -29,6 +29,7 @@ from _pytest.mark.structures import MarkDecorator
|
||||||
from _pytest.mark.structures import NodeKeywords
|
from _pytest.mark.structures import NodeKeywords
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Failed
|
from _pytest.outcomes import Failed
|
||||||
|
from _pytest.store import Store
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Imported here due to circular import.
|
# Imported here due to circular import.
|
||||||
|
@ -146,6 +147,10 @@ class Node(metaclass=NodeMeta):
|
||||||
if self.name != "()":
|
if self.name != "()":
|
||||||
self._nodeid += "::" + self.name
|
self._nodeid += "::" + self.name
|
||||||
|
|
||||||
|
# A place where plugins can store information on the node for their
|
||||||
|
# own use. Currently only intended for internal plugins.
|
||||||
|
self._store = Store()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent: "Node", **kw):
|
def from_parent(cls, parent: "Node", **kw):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
""" submit failure or test session information to a pastebin service. """
|
""" submit failure or test session information to a pastebin service. """
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.store import StoreKey
|
||||||
|
|
||||||
|
|
||||||
|
pastebinfile_key = StoreKey[IO[bytes]]()
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
|
@ -26,25 +31,26 @@ def pytest_configure(config):
|
||||||
# when using pytest-xdist, for example
|
# when using pytest-xdist, for example
|
||||||
if tr is not None:
|
if tr is not None:
|
||||||
# pastebin file will be utf-8 encoded binary file
|
# pastebin file will be utf-8 encoded binary file
|
||||||
config._pastebinfile = tempfile.TemporaryFile("w+b")
|
config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b")
|
||||||
oldwrite = tr._tw.write
|
oldwrite = tr._tw.write
|
||||||
|
|
||||||
def tee_write(s, **kwargs):
|
def tee_write(s, **kwargs):
|
||||||
oldwrite(s, **kwargs)
|
oldwrite(s, **kwargs)
|
||||||
if isinstance(s, str):
|
if isinstance(s, str):
|
||||||
s = s.encode("utf-8")
|
s = s.encode("utf-8")
|
||||||
config._pastebinfile.write(s)
|
config._store[pastebinfile_key].write(s)
|
||||||
|
|
||||||
tr._tw.write = tee_write
|
tr._tw.write = tee_write
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
def pytest_unconfigure(config):
|
||||||
if hasattr(config, "_pastebinfile"):
|
if pastebinfile_key in config._store:
|
||||||
|
pastebinfile = config._store[pastebinfile_key]
|
||||||
# get terminal contents and delete file
|
# get terminal contents and delete file
|
||||||
config._pastebinfile.seek(0)
|
pastebinfile.seek(0)
|
||||||
sessionlog = config._pastebinfile.read()
|
sessionlog = pastebinfile.read()
|
||||||
config._pastebinfile.close()
|
pastebinfile.close()
|
||||||
del config._pastebinfile
|
del config._store[pastebinfile_key]
|
||||||
# undo our patching in the terminal reporter
|
# undo our patching in the terminal reporter
|
||||||
tr = config.pluginmanager.getplugin("terminalreporter")
|
tr = config.pluginmanager.getplugin("terminalreporter")
|
||||||
del tr._tw.__dict__["write"]
|
del tr._tw.__dict__["write"]
|
||||||
|
|
|
@ -5,6 +5,11 @@ import os
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
from _pytest.store import StoreKey
|
||||||
|
|
||||||
|
|
||||||
|
resultlog_key = StoreKey["ResultLog"]()
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
group = parser.getgroup("terminal reporting", "resultlog plugin options")
|
group = parser.getgroup("terminal reporting", "resultlog plugin options")
|
||||||
|
@ -26,8 +31,8 @@ def pytest_configure(config):
|
||||||
if not os.path.isdir(dirname):
|
if not os.path.isdir(dirname):
|
||||||
os.makedirs(dirname)
|
os.makedirs(dirname)
|
||||||
logfile = open(resultlog, "w", 1) # line buffered
|
logfile = open(resultlog, "w", 1) # line buffered
|
||||||
config._resultlog = ResultLog(config, logfile)
|
config._store[resultlog_key] = ResultLog(config, logfile)
|
||||||
config.pluginmanager.register(config._resultlog)
|
config.pluginmanager.register(config._store[resultlog_key])
|
||||||
|
|
||||||
from _pytest.deprecated import RESULT_LOG
|
from _pytest.deprecated import RESULT_LOG
|
||||||
from _pytest.warnings import _issue_warning_captured
|
from _pytest.warnings import _issue_warning_captured
|
||||||
|
@ -36,10 +41,10 @@ def pytest_configure(config):
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
def pytest_unconfigure(config):
|
||||||
resultlog = getattr(config, "_resultlog", None)
|
resultlog = config._store.get(resultlog_key, None)
|
||||||
if resultlog:
|
if resultlog:
|
||||||
resultlog.logfile.close()
|
resultlog.logfile.close()
|
||||||
del config._resultlog
|
del config._store[resultlog_key]
|
||||||
config.pluginmanager.unregister(resultlog)
|
config.pluginmanager.unregister(resultlog)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,12 @@ from _pytest.mark.evaluate import MarkEvaluator
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
from _pytest.outcomes import xfail
|
from _pytest.outcomes import xfail
|
||||||
|
from _pytest.store import StoreKey
|
||||||
|
|
||||||
|
|
||||||
|
skipped_by_mark_key = StoreKey[bool]()
|
||||||
|
evalxfail_key = StoreKey[MarkEvaluator]()
|
||||||
|
unexpectedsuccess_key = StoreKey[str]()
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
|
@ -68,14 +74,14 @@ def pytest_configure(config):
|
||||||
@hookimpl(tryfirst=True)
|
@hookimpl(tryfirst=True)
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
# Check if skip or skipif are specified as pytest marks
|
# Check if skip or skipif are specified as pytest marks
|
||||||
item._skipped_by_mark = False
|
item._store[skipped_by_mark_key] = False
|
||||||
eval_skipif = MarkEvaluator(item, "skipif")
|
eval_skipif = MarkEvaluator(item, "skipif")
|
||||||
if eval_skipif.istrue():
|
if eval_skipif.istrue():
|
||||||
item._skipped_by_mark = True
|
item._store[skipped_by_mark_key] = True
|
||||||
skip(eval_skipif.getexplanation())
|
skip(eval_skipif.getexplanation())
|
||||||
|
|
||||||
for skip_info in item.iter_markers(name="skip"):
|
for skip_info in item.iter_markers(name="skip"):
|
||||||
item._skipped_by_mark = True
|
item._store[skipped_by_mark_key] = True
|
||||||
if "reason" in skip_info.kwargs:
|
if "reason" in skip_info.kwargs:
|
||||||
skip(skip_info.kwargs["reason"])
|
skip(skip_info.kwargs["reason"])
|
||||||
elif skip_info.args:
|
elif skip_info.args:
|
||||||
|
@ -83,7 +89,7 @@ def pytest_runtest_setup(item):
|
||||||
else:
|
else:
|
||||||
skip("unconditional skip")
|
skip("unconditional skip")
|
||||||
|
|
||||||
item._evalxfail = MarkEvaluator(item, "xfail")
|
item._store[evalxfail_key] = MarkEvaluator(item, "xfail")
|
||||||
check_xfail_no_run(item)
|
check_xfail_no_run(item)
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,7 +105,7 @@ def pytest_pyfunc_call(pyfuncitem):
|
||||||
def check_xfail_no_run(item):
|
def check_xfail_no_run(item):
|
||||||
"""check xfail(run=False)"""
|
"""check xfail(run=False)"""
|
||||||
if not item.config.option.runxfail:
|
if not item.config.option.runxfail:
|
||||||
evalxfail = item._evalxfail
|
evalxfail = item._store[evalxfail_key]
|
||||||
if evalxfail.istrue():
|
if evalxfail.istrue():
|
||||||
if not evalxfail.get("run", True):
|
if not evalxfail.get("run", True):
|
||||||
xfail("[NOTRUN] " + evalxfail.getexplanation())
|
xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||||
|
@ -107,12 +113,12 @@ def check_xfail_no_run(item):
|
||||||
|
|
||||||
def check_strict_xfail(pyfuncitem):
|
def check_strict_xfail(pyfuncitem):
|
||||||
"""check xfail(strict=True) for the given PASSING test"""
|
"""check xfail(strict=True) for the given PASSING test"""
|
||||||
evalxfail = pyfuncitem._evalxfail
|
evalxfail = pyfuncitem._store[evalxfail_key]
|
||||||
if evalxfail.istrue():
|
if evalxfail.istrue():
|
||||||
strict_default = pyfuncitem.config.getini("xfail_strict")
|
strict_default = pyfuncitem.config.getini("xfail_strict")
|
||||||
is_strict_xfail = evalxfail.get("strict", strict_default)
|
is_strict_xfail = evalxfail.get("strict", strict_default)
|
||||||
if is_strict_xfail:
|
if is_strict_xfail:
|
||||||
del pyfuncitem._evalxfail
|
del pyfuncitem._store[evalxfail_key]
|
||||||
explanation = evalxfail.getexplanation()
|
explanation = evalxfail.getexplanation()
|
||||||
fail("[XPASS(strict)] " + explanation, pytrace=False)
|
fail("[XPASS(strict)] " + explanation, pytrace=False)
|
||||||
|
|
||||||
|
@ -121,12 +127,12 @@ def check_strict_xfail(pyfuncitem):
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
outcome = yield
|
outcome = yield
|
||||||
rep = outcome.get_result()
|
rep = outcome.get_result()
|
||||||
evalxfail = getattr(item, "_evalxfail", None)
|
evalxfail = item._store.get(evalxfail_key, None)
|
||||||
# unittest special case, see setting of _unexpectedsuccess
|
# unittest special case, see setting of unexpectedsuccess_key
|
||||||
if hasattr(item, "_unexpectedsuccess") and rep.when == "call":
|
if unexpectedsuccess_key in item._store and rep.when == "call":
|
||||||
|
reason = item._store[unexpectedsuccess_key]
|
||||||
if item._unexpectedsuccess:
|
if reason:
|
||||||
rep.longrepr = "Unexpected success: {}".format(item._unexpectedsuccess)
|
rep.longrepr = "Unexpected success: {}".format(reason)
|
||||||
else:
|
else:
|
||||||
rep.longrepr = "Unexpected success"
|
rep.longrepr = "Unexpected success"
|
||||||
rep.outcome = "failed"
|
rep.outcome = "failed"
|
||||||
|
@ -154,7 +160,7 @@ def pytest_runtest_makereport(item, call):
|
||||||
rep.outcome = "passed"
|
rep.outcome = "passed"
|
||||||
rep.wasxfail = explanation
|
rep.wasxfail = explanation
|
||||||
elif (
|
elif (
|
||||||
getattr(item, "_skipped_by_mark", False)
|
item._store.get(skipped_by_mark_key, True)
|
||||||
and rep.skipped
|
and rep.skipped
|
||||||
and type(rep.longrepr) is tuple
|
and type(rep.longrepr) is tuple
|
||||||
):
|
):
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
from typing import Any
|
||||||
|
from typing import cast
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Generic
|
||||||
|
from typing import TypeVar
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Store", "StoreKey"]
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
D = TypeVar("D")
|
||||||
|
|
||||||
|
|
||||||
|
class StoreKey(Generic[T]):
|
||||||
|
"""StoreKey is an object used as a key to a Store.
|
||||||
|
|
||||||
|
A StoreKey is associated with the type T of the value of the key.
|
||||||
|
|
||||||
|
A StoreKey is unique and cannot conflict with another key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
||||||
|
class Store:
|
||||||
|
"""Store is a type-safe heterogenous mutable mapping that
|
||||||
|
allows keys and value types to be defined separately from
|
||||||
|
where it is defined.
|
||||||
|
|
||||||
|
Usually you will be given an object which has a ``Store``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
store: Store = some_object.store
|
||||||
|
|
||||||
|
If a module wants to store data in this Store, it creates StoreKeys
|
||||||
|
for its keys (at the module level):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
some_str_key = StoreKey[str]()
|
||||||
|
some_bool_key = StoreKey[bool]()
|
||||||
|
|
||||||
|
To store information:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Value type must match the key.
|
||||||
|
store[some_str_key] = "value"
|
||||||
|
store[some_bool_key] = True
|
||||||
|
|
||||||
|
To retrieve the information:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# The static type of some_str is str.
|
||||||
|
some_str = store[some_str_key]
|
||||||
|
# The static type of some_bool is bool.
|
||||||
|
some_bool = store[some_bool_key]
|
||||||
|
|
||||||
|
Why use this?
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Problem: module Internal defines an object. Module External, which
|
||||||
|
module Internal doesn't know about, receives the object and wants to
|
||||||
|
attach information to it, to be retrieved later given the object.
|
||||||
|
|
||||||
|
Bad solution 1: Module External assigns private attributes directly on
|
||||||
|
the object. This doesn't work well because the type checker doesn't
|
||||||
|
know about these attributes and it complains about undefined attributes.
|
||||||
|
|
||||||
|
Bad solution 2: module Internal adds a ``Dict[str, Any]`` attribute to
|
||||||
|
the object. Module External stores its data in private keys of this dict.
|
||||||
|
This doesn't work well because retrieved values are untyped.
|
||||||
|
|
||||||
|
Good solution: module Internal adds a ``Store`` to the object. Module
|
||||||
|
External mints StoreKeys for its own keys. Module External stores and
|
||||||
|
retrieves its data using its keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_store",)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._store = {} # type: Dict[StoreKey[Any], object]
|
||||||
|
|
||||||
|
def __setitem__(self, key: StoreKey[T], value: T) -> None:
|
||||||
|
"""Set a value for key."""
|
||||||
|
self._store[key] = value
|
||||||
|
|
||||||
|
def __getitem__(self, key: StoreKey[T]) -> T:
|
||||||
|
"""Get the value for key.
|
||||||
|
|
||||||
|
Raises KeyError if the key wasn't set before.
|
||||||
|
"""
|
||||||
|
return cast(T, self._store[key])
|
||||||
|
|
||||||
|
def get(self, key: StoreKey[T], default: D) -> Union[T, D]:
|
||||||
|
"""Get the value for key, or return default if the key wasn't set
|
||||||
|
before."""
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def __delitem__(self, key: StoreKey[T]) -> None:
|
||||||
|
"""Delete the value for key.
|
||||||
|
|
||||||
|
Raises KeyError if the key wasn't set before.
|
||||||
|
"""
|
||||||
|
del self._store[key]
|
||||||
|
|
||||||
|
def __contains__(self, key: StoreKey[T]) -> bool:
|
||||||
|
"""Returns whether key was set."""
|
||||||
|
return key in self._store
|
|
@ -14,6 +14,8 @@ from _pytest.outcomes import xfail
|
||||||
from _pytest.python import Class
|
from _pytest.python import Class
|
||||||
from _pytest.python import Function
|
from _pytest.python import Function
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
|
from _pytest.skipping import skipped_by_mark_key
|
||||||
|
from _pytest.skipping import unexpectedsuccess_key
|
||||||
|
|
||||||
|
|
||||||
def pytest_pycollect_makeitem(collector, name, obj):
|
def pytest_pycollect_makeitem(collector, name, obj):
|
||||||
|
@ -174,7 +176,7 @@ class TestCaseFunction(Function):
|
||||||
try:
|
try:
|
||||||
skip(reason)
|
skip(reason)
|
||||||
except skip.Exception:
|
except skip.Exception:
|
||||||
self._skipped_by_mark = True
|
self._store[skipped_by_mark_key] = True
|
||||||
self._addexcinfo(sys.exc_info())
|
self._addexcinfo(sys.exc_info())
|
||||||
|
|
||||||
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
||||||
|
@ -184,7 +186,7 @@ class TestCaseFunction(Function):
|
||||||
self._addexcinfo(sys.exc_info())
|
self._addexcinfo(sys.exc_info())
|
||||||
|
|
||||||
def addUnexpectedSuccess(self, testcase, reason=""):
|
def addUnexpectedSuccess(self, testcase, reason=""):
|
||||||
self._unexpectedsuccess = reason
|
self._store[unexpectedsuccess_key] = reason
|
||||||
|
|
||||||
def addSuccess(self, testcase):
|
def addSuccess(self, testcase):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -10,6 +10,7 @@ import pytest
|
||||||
from _pytest.junitxml import LogXML
|
from _pytest.junitxml import LogXML
|
||||||
from _pytest.pathlib import Path
|
from _pytest.pathlib import Path
|
||||||
from _pytest.reports import BaseReport
|
from _pytest.reports import BaseReport
|
||||||
|
from _pytest.store import Store
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
@ -865,6 +866,7 @@ def test_dont_configure_on_slaves(tmpdir):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.pluginmanager = self
|
self.pluginmanager = self
|
||||||
self.option = self
|
self.option = self
|
||||||
|
self._store = Store()
|
||||||
|
|
||||||
def getini(self, name):
|
def getini(self, name):
|
||||||
return "pytest"
|
return "pytest"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import pytest
|
||||||
from _pytest.resultlog import pytest_configure
|
from _pytest.resultlog import pytest_configure
|
||||||
from _pytest.resultlog import pytest_unconfigure
|
from _pytest.resultlog import pytest_unconfigure
|
||||||
from _pytest.resultlog import ResultLog
|
from _pytest.resultlog import ResultLog
|
||||||
|
from _pytest.resultlog import resultlog_key
|
||||||
|
|
||||||
pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated")
|
pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated")
|
||||||
|
|
||||||
|
@ -179,17 +180,17 @@ def test_makedir_for_resultlog(testdir, LineMatcher):
|
||||||
def test_no_resultlog_on_slaves(testdir):
|
def test_no_resultlog_on_slaves(testdir):
|
||||||
config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog")
|
config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog")
|
||||||
|
|
||||||
assert not hasattr(config, "_resultlog")
|
assert resultlog_key not in config._store
|
||||||
pytest_configure(config)
|
pytest_configure(config)
|
||||||
assert hasattr(config, "_resultlog")
|
assert resultlog_key in config._store
|
||||||
pytest_unconfigure(config)
|
pytest_unconfigure(config)
|
||||||
assert not hasattr(config, "_resultlog")
|
assert resultlog_key not in config._store
|
||||||
|
|
||||||
config.slaveinput = {}
|
config.slaveinput = {}
|
||||||
pytest_configure(config)
|
pytest_configure(config)
|
||||||
assert not hasattr(config, "_resultlog")
|
assert resultlog_key not in config._store
|
||||||
pytest_unconfigure(config)
|
pytest_unconfigure(config)
|
||||||
assert not hasattr(config, "_resultlog")
|
assert resultlog_key not in config._store
|
||||||
|
|
||||||
|
|
||||||
def test_failure_issue380(testdir):
|
def test_failure_issue380(testdir):
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import pytest
|
||||||
|
from _pytest.store import Store
|
||||||
|
from _pytest.store import StoreKey
|
||||||
|
|
||||||
|
|
||||||
|
def test_store() -> None:
|
||||||
|
store = Store()
|
||||||
|
|
||||||
|
key1 = StoreKey[str]()
|
||||||
|
key2 = StoreKey[int]()
|
||||||
|
|
||||||
|
# Basic functionality - single key.
|
||||||
|
assert key1 not in store
|
||||||
|
store[key1] = "hello"
|
||||||
|
assert key1 in store
|
||||||
|
assert store[key1] == "hello"
|
||||||
|
assert store.get(key1, None) == "hello"
|
||||||
|
store[key1] = "world"
|
||||||
|
assert store[key1] == "world"
|
||||||
|
# Has correct type (no mypy error).
|
||||||
|
store[key1] + "string"
|
||||||
|
|
||||||
|
# No interaction with another key.
|
||||||
|
assert key2 not in store
|
||||||
|
assert store.get(key2, None) is None
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
store[key2]
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
del store[key2]
|
||||||
|
store[key2] = 1
|
||||||
|
assert store[key2] == 1
|
||||||
|
# Has correct type (no mypy error).
|
||||||
|
store[key2] + 20
|
||||||
|
del store[key1]
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
del store[key1]
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
store[key1]
|
||||||
|
|
||||||
|
# Can't accidentally add attributes to store object itself.
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
store.foo = "nope" # type: ignore[attr-defined] # noqa: F821
|
||||||
|
|
||||||
|
# No interaction with anoter store.
|
||||||
|
store2 = Store()
|
||||||
|
key3 = StoreKey[int]()
|
||||||
|
assert key2 not in store2
|
||||||
|
store2[key2] = 100
|
||||||
|
store2[key3] = 200
|
||||||
|
assert store2[key2] + store2[key3] == 300
|
||||||
|
assert store[key2] == 1
|
||||||
|
assert key3 not in store
|
Loading…
Reference in New Issue