Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2390610696 | ||
|
|
a0714aa007 | ||
|
|
44ad1c9811 | ||
|
|
5dc77253d4 | ||
|
|
a517827318 | ||
|
|
21fe071d79 | ||
|
|
f8bb8572fe | ||
|
|
1944dc06d3 | ||
|
|
946634c84c | ||
|
|
d849a3ed64 | ||
|
|
721a0881fb | ||
|
|
5341b9cd67 | ||
|
|
c39bdf6190 | ||
|
|
b0c4775a28 |
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -36,8 +36,10 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -9,6 +9,10 @@ python:
|
||||
path: .
|
||||
- requirements: doc/en/requirements.txt
|
||||
|
||||
sphinx:
|
||||
configuration: doc/en/conf.py
|
||||
fail_on_warning: true
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
|
||||
2
AUTHORS
2
AUTHORS
@@ -231,6 +231,7 @@ Maho
|
||||
Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
@@ -336,6 +337,7 @@ Serhii Mozghovyi
|
||||
Seth Junot
|
||||
Shantanu Jain
|
||||
Shubham Adep
|
||||
Simon Blanchard
|
||||
Simon Gomizelj
|
||||
Simon Holesch
|
||||
Simon Kerr
|
||||
|
||||
@@ -134,7 +134,8 @@ Releasing
|
||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
||||
|
||||
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml.
|
||||
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||
as source.
|
||||
|
||||
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||
and tag the repository.
|
||||
|
||||
@@ -6,6 +6,7 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.4.3
|
||||
release-7.4.2
|
||||
release-7.4.1
|
||||
release-7.4.0
|
||||
|
||||
19
doc/en/announce/release-7.4.3.rst
Normal file
19
doc/en/announce/release-7.4.3.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
pytest-7.4.3
|
||||
=======================================
|
||||
|
||||
pytest 7.4.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Marc Mueller
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -28,6 +28,21 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.4.3 (2023-10-24)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression.
|
||||
|
||||
|
||||
- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
|
||||
|
||||
|
||||
- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
|
||||
|
||||
|
||||
pytest 7.4.2 (2023-09-07)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.4.2
|
||||
pytest 7.4.3
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
||||
setup.cfg
|
||||
~~~~~~~~~
|
||||
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
|
||||
if they have a ``[tool:pytest]`` section.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@@ -31,10 +31,16 @@ class InvalidFeatureRelease(Exception):
|
||||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
PR_BODY = """\
|
||||
Created automatically from manual trigger.
|
||||
Created by the [prepare release pr](https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml)
|
||||
workflow.
|
||||
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
Once all builds pass and it has been **approved** by one or more maintainers,
|
||||
start the [deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
|
||||
|
||||
* `Use workflow from`: `release-{version}`.
|
||||
* `Release version`: `{version}`.
|
||||
|
||||
After the `deploy` workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import struct
|
||||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
@@ -56,6 +57,10 @@ else:
|
||||
astNum = ast.Num
|
||||
|
||||
|
||||
class Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
|
||||
# pytest caches rewritten pycs in pycache dirs
|
||||
@@ -63,6 +68,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
# Special marker that denotes we have just left a scope definition
|
||||
_SCOPE_END_MARKER = Sentinel()
|
||||
|
||||
|
||||
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||
@@ -645,6 +653,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
:scope: A tuple containing the current scope used for variables_overwrite.
|
||||
|
||||
:variables_overwrite: A dict filled with references to variables
|
||||
that change value within an assert. This happens when a variable is
|
||||
reassigned with the walrus operator
|
||||
@@ -666,7 +676,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
else:
|
||||
self.enable_assertion_pass_hook = False
|
||||
self.source = source
|
||||
self.variables_overwrite: Dict[str, str] = {}
|
||||
self.scope: tuple[ast.AST, ...] = ()
|
||||
self.variables_overwrite: defaultdict[
|
||||
tuple[ast.AST, ...], Dict[str, str]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def run(self, mod: ast.Module) -> None:
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -732,9 +745,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
mod.body[pos:pos] = imports
|
||||
|
||||
# Collect asserts.
|
||||
nodes: List[ast.AST] = [mod]
|
||||
self.scope = (mod,)
|
||||
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
self.scope = tuple((*self.scope, node))
|
||||
nodes.append(_SCOPE_END_MARKER)
|
||||
if node == _SCOPE_END_MARKER:
|
||||
self.scope = self.scope[:-1]
|
||||
continue
|
||||
assert isinstance(node, ast.AST)
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new: List[ast.AST] = []
|
||||
@@ -1005,7 +1026,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
@@ -1048,17 +1069,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
||||
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][
|
||||
arg.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
if (
|
||||
isinstance(keyword.value, ast.Name)
|
||||
and keyword.value.id in self.variables_overwrite
|
||||
):
|
||||
keyword.value = self.variables_overwrite[
|
||||
if isinstance(
|
||||
keyword.value, ast.Name
|
||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||
keyword.value = self.variables_overwrite[self.scope][
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
@@ -1094,12 +1118,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
||||
self.push_format_context()
|
||||
# We first check if we have overwritten a variable in the previous assert
|
||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
||||
comp.left = self.variables_overwrite[
|
||||
if isinstance(
|
||||
comp.left, ast.Name
|
||||
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
||||
comp.left = self.variables_overwrite[self.scope][
|
||||
comp.left.id
|
||||
] # type:ignore[assignment]
|
||||
if isinstance(comp.left, namedExpr):
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
@@ -1119,7 +1145,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
|
||||
@@ -380,15 +380,24 @@ else:
|
||||
|
||||
|
||||
def get_user_id() -> int | None:
|
||||
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
||||
# win32 does not have a getuid() function.
|
||||
# On Emscripten, getuid() is a stub that always returns 0.
|
||||
if sys.platform in ("win32", "emscripten"):
|
||||
"""Return the current process's real user id or None if it could not be
|
||||
determined.
|
||||
|
||||
:return: The user id or None if it could not be determined.
|
||||
"""
|
||||
# mypy follows the version and platform checking expectation of PEP 484:
|
||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||
# win32 does not have a getuid() function.
|
||||
# Emscripten has a return 0 stub.
|
||||
return None
|
||||
# getuid shouldn't fail, but cpython defines such a case.
|
||||
# Let's hope for the best.
|
||||
uid = os.getuid()
|
||||
return uid if uid != -1 else None
|
||||
else:
|
||||
# On other platforms, a return value of -1 is assumed to indicate that
|
||||
# the current process's real user id could not be determined.
|
||||
ERROR = -1
|
||||
uid = os.getuid()
|
||||
return uid if uid != ERROR else None
|
||||
|
||||
|
||||
# Perform exhaustiveness checking.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import Generator
|
||||
@@ -51,7 +50,7 @@ def get_stderr_fileno() -> int:
|
||||
if fileno == -1:
|
||||
raise AttributeError()
|
||||
return fileno
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
except (AttributeError, ValueError):
|
||||
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||
# This is potentially dangerous, but the best we can do.
|
||||
|
||||
@@ -373,7 +373,9 @@ def get_unpacked_marks(
|
||||
if not consider_mro:
|
||||
mark_lists = [obj.__dict__.get("pytestmark", [])]
|
||||
else:
|
||||
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
|
||||
mark_lists = [
|
||||
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
|
||||
]
|
||||
mark_list = []
|
||||
for item in mark_lists:
|
||||
if isinstance(item, list):
|
||||
|
||||
@@ -623,8 +623,9 @@ def module_name_from_path(path: Path, root: Path) -> str:
|
||||
# Use the parts for the relative path to the root path.
|
||||
path_parts = relative_path.parts
|
||||
|
||||
# Module name for packages do not contain the __init__ file.
|
||||
if path_parts[-1] == "__init__":
|
||||
# Module name for packages do not contain the __init__ file, unless
|
||||
# the `__init__.py` file is at the root.
|
||||
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
|
||||
path_parts = path_parts[:-1]
|
||||
|
||||
return ".".join(path_parts)
|
||||
|
||||
@@ -21,6 +21,15 @@ if sys.gettrace():
|
||||
sys.settrace(orig_trace)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Force terminal width to 80: some tests check the formatting of --help, which is sensible
|
||||
to terminal width.
|
||||
"""
|
||||
monkeypatch.setenv("COLUMNS", "80")
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
"""Prefer faster tests.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# mypy: disable-error-code="attr-defined"
|
||||
# mypy: disallow-untyped-defs
|
||||
import logging
|
||||
from typing import Iterator
|
||||
|
||||
import pytest
|
||||
from _pytest.logging import caplog_records_key
|
||||
@@ -9,8 +11,8 @@ logger = logging.getLogger(__name__)
|
||||
sublogger = logging.getLogger(__name__ + ".baz")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleanup_disabled_logging():
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_disabled_logging() -> Iterator[None]:
|
||||
"""Simple fixture that ensures that a test doesn't disable logging.
|
||||
|
||||
This is necessary because ``logging.disable()`` is global, so a test disabling logging
|
||||
@@ -27,7 +29,7 @@ def test_fixture_help(pytester: Pytester) -> None:
|
||||
result.stdout.fnmatch_lines(["*caplog*"])
|
||||
|
||||
|
||||
def test_change_level(caplog):
|
||||
def test_change_level(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
@@ -42,7 +44,7 @@ def test_change_level(caplog):
|
||||
assert "CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_change_level_logging_disabled(caplog, cleanup_disabled_logging):
|
||||
def test_change_level_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
|
||||
logging.disable(logging.CRITICAL)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
caplog.set_level(logging.WARNING)
|
||||
@@ -85,9 +87,7 @@ def test_change_level_undo(pytester: Pytester) -> None:
|
||||
result.stdout.no_fnmatch_line("*log from test2*")
|
||||
|
||||
|
||||
def test_change_disabled_level_undo(
|
||||
pytester: Pytester, cleanup_disabled_logging
|
||||
) -> None:
|
||||
def test_change_disabled_level_undo(pytester: Pytester) -> None:
|
||||
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
|
||||
|
||||
Tests the logging output themselves (affected by disabled logging level).
|
||||
@@ -144,7 +144,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||
result.assert_outcomes(passed=3)
|
||||
|
||||
|
||||
def test_with_statement(caplog):
|
||||
def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level(logging.INFO):
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
@@ -159,7 +159,7 @@ def test_with_statement(caplog):
|
||||
assert "CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
|
||||
def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
|
||||
logging.disable(logging.CRITICAL)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
with caplog.at_level(logging.WARNING):
|
||||
@@ -198,8 +198,8 @@ def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
|
||||
],
|
||||
)
|
||||
def test_force_enable_logging_level_string(
|
||||
caplog, cleanup_disabled_logging, level_str, expected_disable_level
|
||||
):
|
||||
caplog: pytest.LogCaptureFixture, level_str: str, expected_disable_level: int
|
||||
) -> None:
|
||||
"""Test _force_enable_logging using a level string.
|
||||
|
||||
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
|
||||
@@ -218,7 +218,7 @@ def test_force_enable_logging_level_string(
|
||||
assert test_logger.manager.disable == expected_disable_level
|
||||
|
||||
|
||||
def test_log_access(caplog):
|
||||
def test_log_access(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
assert caplog.records[0].levelname == "INFO"
|
||||
@@ -226,7 +226,7 @@ def test_log_access(caplog):
|
||||
assert "boo arg" in caplog.text
|
||||
|
||||
|
||||
def test_messages(caplog):
|
||||
def test_messages(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
logger.info("bar %s\nbaz %s", "arg1", "arg2")
|
||||
@@ -247,14 +247,14 @@ def test_messages(caplog):
|
||||
assert "Exception" not in caplog.messages[-1]
|
||||
|
||||
|
||||
def test_record_tuples(caplog):
|
||||
def test_record_tuples(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
|
||||
assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
|
||||
|
||||
|
||||
def test_unicode(caplog):
|
||||
def test_unicode(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("bū")
|
||||
assert caplog.records[0].levelname == "INFO"
|
||||
@@ -262,7 +262,7 @@ def test_unicode(caplog):
|
||||
assert "bū" in caplog.text
|
||||
|
||||
|
||||
def test_clear(caplog):
|
||||
def test_clear(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("bū")
|
||||
assert len(caplog.records)
|
||||
@@ -273,7 +273,9 @@ def test_clear(caplog):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logging_during_setup_and_teardown(caplog):
|
||||
def logging_during_setup_and_teardown(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> Iterator[None]:
|
||||
caplog.set_level("INFO")
|
||||
logger.info("a_setup_log")
|
||||
yield
|
||||
@@ -281,7 +283,9 @@ def logging_during_setup_and_teardown(caplog):
|
||||
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
|
||||
|
||||
|
||||
def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
|
||||
def test_caplog_captures_for_all_stages(
|
||||
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
||||
) -> None:
|
||||
assert not caplog.records
|
||||
assert not caplog.get_records("call")
|
||||
logger.info("a_call_log")
|
||||
@@ -290,25 +294,31 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
|
||||
# This reaches into private API, don't use this type of thing in real tests!
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
|
||||
def test_clear_for_call_stage(caplog, logging_during_setup_and_teardown):
|
||||
def test_clear_for_call_stage(
|
||||
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
||||
) -> None:
|
||||
logger.info("a_call_log")
|
||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
caplog.clear()
|
||||
|
||||
assert caplog.get_records("call") == []
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
logging.info("a_call_log_after_clear")
|
||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
|
||||
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
||||
|
||||
@@ -1531,6 +1531,28 @@ class TestIssue11028:
|
||||
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
|
||||
|
||||
|
||||
class TestIssue11239:
|
||||
@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Only Python 3.8+")
|
||||
def test_assertion_walrus_different_test_cases(self, pytester: Pytester) -> None:
|
||||
"""Regression for (#11239)
|
||||
|
||||
Walrus operator rewriting would leak to separate test cases if they used the same variables.
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_1():
|
||||
state = {"x": 2}.get("x")
|
||||
assert state is not None
|
||||
|
||||
def test_2():
|
||||
db = {"x": 2}
|
||||
assert (state := db.get("x")) is not None
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
)
|
||||
|
||||
@@ -1130,6 +1130,41 @@ def test_mark_mro() -> None:
|
||||
|
||||
all_marks = get_unpacked_marks(C)
|
||||
|
||||
assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark]
|
||||
assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark]
|
||||
|
||||
assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark]
|
||||
|
||||
|
||||
# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/10447")
|
||||
def test_mark_fixture_order_mro(pytester: Pytester):
|
||||
"""This ensures we walk marks of the mro starting with the base classes
|
||||
the action at a distance fixtures are taken as minimal example from a real project
|
||||
|
||||
"""
|
||||
foo = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def add_attr1(request):
|
||||
request.instance.attr1 = object()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def add_attr2(request):
|
||||
request.instance.attr2 = request.instance.attr1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('add_attr1')
|
||||
class Parent:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('add_attr2')
|
||||
class TestThings(Parent):
|
||||
def test_attrs(self):
|
||||
assert self.attr1 == self.attr2
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(foo)
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
@@ -291,7 +291,8 @@ class TestParser:
|
||||
|
||||
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
||||
try:
|
||||
encoding = locale.getencoding() # New in Python 3.11, ignores utf-8 mode
|
||||
# New in Python 3.11, ignores utf-8 mode
|
||||
encoding = locale.getencoding() # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
encoding = locale.getpreferredencoding(False)
|
||||
try:
|
||||
|
||||
@@ -28,6 +28,7 @@ from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.pathlib import symlink_or_skip
|
||||
from _pytest.pathlib import visit
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
|
||||
|
||||
@@ -592,6 +593,10 @@ class TestImportLibMode:
|
||||
result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
|
||||
assert result == "src.app"
|
||||
|
||||
# Unless __init__.py file is at the root, in which case we cannot have an empty module name.
|
||||
result = module_name_from_path(tmp_path / "__init__.py", tmp_path)
|
||||
assert result == "__init__"
|
||||
|
||||
def test_insert_missing_modules(
|
||||
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
@@ -663,6 +668,22 @@ class TestImportLibMode:
|
||||
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
|
||||
assert len(mod.instance.INSTANCES) == 1
|
||||
|
||||
def test_importlib_root_is_package(self, pytester: Pytester) -> None:
|
||||
"""
|
||||
Regression for importing a `__init__`.py file that is at the root
|
||||
(#11417).
|
||||
"""
|
||||
pytester.makepyfile(__init__="")
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_my_test():
|
||||
assert True
|
||||
"""
|
||||
)
|
||||
|
||||
result = pytester.runpytest("--import-mode=importlib")
|
||||
result.stdout.fnmatch_lines("* 1 passed *")
|
||||
|
||||
|
||||
def test_safe_exists(tmp_path: Path) -> None:
|
||||
d = tmp_path.joinpath("some_dir")
|
||||
|
||||
Reference in New Issue
Block a user