Compare commits

..

2 Commits

Author SHA1 Message Date
Ran Benita
ec8e23951d Merge pull request #10883 from bluetech/cherry-pick-release
Cherry pick 7.3.0 release notes
2023-04-09 00:53:52 +03:00
Ran Benita
bf47357511 Merge pull request #10881 from pytest-dev/release-7.3.0
Prepare release 7.3.0

(cherry picked from commit cec5bfe058)
2023-04-09 00:50:37 +03:00
43 changed files with 174 additions and 502 deletions

View File

@@ -43,7 +43,6 @@ jobs:
"windows-py39",
"windows-py310",
"windows-py311",
"windows-py312",
"ubuntu-py37",
"ubuntu-py37-pluggy",
@@ -52,13 +51,12 @@ jobs:
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3",
"macos-py37",
"macos-py38",
"macos-py39",
"macos-py310",
"macos-py312",
"docs",
"doctesting",
@@ -88,13 +86,9 @@ jobs:
os: windows-latest
tox_env: "py310-xdist"
- name: "windows-py311"
python: "3.11"
python: "3.11-dev"
os: windows-latest
tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"
- name: "ubuntu-py37"
python: "3.7"
@@ -122,15 +116,10 @@ jobs:
os: ubuntu-latest
tox_env: "py310-xdist"
- name: "ubuntu-py311"
python: "3.11"
python: "3.11-dev"
os: ubuntu-latest
tox_env: "py311"
use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3"
python: "pypy-3.7"
os: ubuntu-latest
@@ -140,19 +129,19 @@ jobs:
python: "3.7"
os: macos-latest
tox_env: "py37-xdist"
- name: "macos-py38"
python: "3.8"
os: macos-latest
tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39"
python: "3.9"
os: macos-latest
tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310"
python: "3.10"
os: macos-latest
tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"
- name: "plugins"
python: "3.9"
@@ -179,7 +168,6 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies
run: |

View File

@@ -52,7 +52,7 @@ repos:
rev: v2.2.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.12", "--include-version-classifiers"]
args: ["--max-py-version=3.11", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:

View File

@@ -8,7 +8,6 @@ Abdeali JK
Abdelrahman Elbehery
Abhijeet Kasurde
Adam Johnson
Adam Stewart
Adam Uhlir
Ahn Ki-Wook
Akiomi Kamakura
@@ -129,6 +128,7 @@ Erik M. Bray
Evan Kepner
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
Florian Bruhin

View File

@@ -6,8 +6,6 @@ Release announcements
:maxdepth: 2
release-7.3.2
release-7.3.1
release-7.3.0
release-7.2.2
release-7.2.1

View File

@@ -1,18 +0,0 @@
pytest-7.3.1
=======================================
pytest 7.3.1 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:
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -1,21 +0,0 @@
pytest-7.3.2
=======================================
pytest 7.3.2 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:
* Adam J. Stewart
* Alessio Izzo
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -92,5 +92,3 @@ pytest version min. Python version
5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+
============== ===================
`Status of Python Versions <https://devguide.python.org/versions/>`__.

View File

@@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:30
monkeypatch -- .../_pytest/monkeypatch.py:29
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or

View File

@@ -28,53 +28,6 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 7.3.2 (2023-06-10)
=========================
Bug Fixes
---------
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
pytest 7.3.1 (2023-04-14)
=========================
Improvements
------------
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
Bug Fixes
---------
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
pytest 7.3.0 (2023-04-08)
=========================
@@ -129,7 +82,6 @@ Bug Fixes
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
NOTE: This change was reverted in version 7.3.1.
@@ -143,12 +95,7 @@ Improved Documentation
Trivial/Internal Changes
------------------------
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
Nice diffs for ``attrs`` classes are still supported though.
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).
pytest 7.2.2 (2023-03-03)
@@ -591,7 +538,7 @@ Breaking Changes
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
@@ -4091,7 +4038,7 @@ Removals
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
Use ``Node.get_closest_marker(name)`` as a replacement.

View File

@@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
# The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = ''
# The unique identifier of the text. This can be an ISBN number
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
# epub_identifier = ''

View File

@@ -38,7 +38,6 @@ class YamlItem(pytest.Item):
" no further details known at this point.",
]
)
return super().repr_failure(excinfo)
def reportinfo(self):
return self.path, 0, f"usecase: {self.name}"

View File

@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 7.3.2
pytest 7.3.0
.. _`simpletest`:

View File

@@ -1,10 +1,11 @@
:orphan:
.. sidebar:: Next Open Trainings
..
.. sidebar:: Next Open Trainings
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote
Also see :doc:`previous talks and blogposts <talks>`.
Also see :doc:`previous talks and blogposts <talks>`.
.. _features:

View File

@@ -1049,11 +1049,11 @@ Environment variables that can be used to change pytest's behavior.
.. envvar:: CI
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
.. envvar:: BUILD_NUMBER
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
.. envvar:: PYTEST_ADDOPTS
@@ -1713,12 +1713,13 @@ passed multiple times. The expected format is ``name=value``. For example::
.. confval:: testpaths
Sets list of directories that should be searched for tests when
no specific directories, files or test ids are given in the command line when
executing pytest from the :ref:`rootdir <rootdir>` directory.
File system paths may use shell-style wildcards, including the recursive
``**`` pattern.
Useful when all project tests are in a known location to speed up
test collection and to avoid picking up undesired tests by accident.
@@ -1727,17 +1728,8 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
testpaths = testing doc
This configuration means that executing:
.. code-block:: console
pytest
has the same practical effects as executing:
.. code-block:: console
pytest testing doc
This tells pytest to only look for tests in ``testing`` and ``doc``
directories when executing from the root directory.
.. confval:: tmp_path_retention_count
@@ -1752,7 +1744,7 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
tmp_path_retention_count = 3
Default: ``3``
Default: 3
.. confval:: tmp_path_retention_policy
@@ -1771,7 +1763,7 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
tmp_path_retention_policy = "all"
Default: ``all``
Default: all
.. confval:: usefixtures
@@ -2004,7 +1996,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
Auto-indent multiline messages passed to the logging
module. Accepts true|on, false|off or an integer.
--log-disable=LOGGER_DISABLE
Disable a logger by name. Can be passed multiple
Disable a logger by name. Can be passed multipe
times.
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:

View File

@@ -22,7 +22,6 @@ classifiers =
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing
Topic :: Utilities
@@ -74,7 +73,6 @@ testing =
nose
pygments>=2.7.2
requests
setuptools
xmlschema
[options.package_data]

View File

@@ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]):
"""
return Traceback(filter(fn, self), self._excinfo)
def getcrashentry(self) -> TracebackEntry:
def getcrashentry(self) -> Optional[TracebackEntry]:
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
for i in range(-1, -len(self) - 1, -1):
entry = self[i]
if not entry.ishidden():
return entry
return self[-1]
return None
def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if
@@ -602,11 +602,13 @@ class ExceptionInfo(Generic[E]):
"""
return isinstance(self.value, exc)
def _getreprcrash(self) -> "ReprFileLocation":
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry()
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
if entry:
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
return None
def getrepr(
self,
@@ -944,9 +946,14 @@ class FormattedExcinfo:
)
else:
reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = (
excinfo_._getreprcrash() if self.style != "value" else None
)
# will be None if all traceback entries are hidden
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
if reprcrash:
if self.style == "value":
repr_chain += [(reprtraceback, None, descr)]
else:
repr_chain += [(reprtraceback, reprcrash, descr)]
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
@@ -954,8 +961,8 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None)
)
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]
repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = (
@@ -1046,7 +1053,7 @@ class ExceptionChainRepr(ExceptionRepr):
@dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation"
reprcrash: Optional["ReprFileLocation"]
def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw)

View File

@@ -953,7 +953,7 @@ class LocalPath:
else:
p.dirpath()._ensuredirs()
if not p.check(file=1):
p.open("wb").close()
p.open("w").close()
return p
@overload

View File

@@ -46,14 +46,8 @@ if TYPE_CHECKING:
if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
astNameConstant = ast.Constant
astStr = ast.Constant
astNum = ast.Constant
else:
namedExpr = ast.Expr
astNameConstant = ast.NameConstant
astStr = ast.Str
astNum = ast.Num
assertstate_key = StashKey["AssertionState"]()
@@ -686,12 +680,9 @@ class AssertionRewriter(ast.NodeVisitor):
if (
expect_docstring
and isinstance(item, ast.Expr)
and isinstance(item.value, astStr)
and isinstance(item.value, ast.Str)
):
if sys.version_info >= (3, 8):
doc = item.value.value
else:
doc = item.value.s
doc = item.value.s
if self.is_rewrite_disabled(doc):
return
expect_docstring = False
@@ -823,7 +814,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop()
if self.stack:
self.explanation_specifiers = self.stack[-1]
keys = [astStr(key) for key in current.keys()]
keys = [ast.Str(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter))
@@ -877,16 +868,16 @@ class AssertionRewriter(ast.NodeVisitor):
negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
msg = self.pop_format_context(astStr(explanation))
msg = self.pop_format_context(ast.Str(explanation))
# Failed
if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert "
else:
assertmsg = astStr("")
assertmsg = ast.Str("")
gluestr = "assert "
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg)
@@ -902,8 +893,8 @@ class AssertionRewriter(ast.NodeVisitor):
hook_call_pass = ast.Expr(
self.helper(
"_call_assertion_pass",
astNum(assert_.lineno),
astStr(orig),
ast.Num(assert_.lineno),
ast.Str(orig),
fmt_pass,
)
)
@@ -922,7 +913,7 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [
ast.Name(name, ast.Store()) for name in self.format_variables
]
clear_format = ast.Assign(variables, astNameConstant(None))
clear_format = ast.Assign(variables, ast.NameConstant(None))
self.statements.append(clear_format)
else: # Original assertion rewriting
@@ -933,9 +924,9 @@ class AssertionRewriter(ast.NodeVisitor):
assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation
else:
assertmsg = astStr("")
assertmsg = ast.Str("")
explanation = "assert " + explanation
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load())
@@ -947,7 +938,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Clear temporary variables by setting them to None.
if self.variables:
variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, astNameConstant(None))
clear = ast.Assign(variables, ast.NameConstant(None))
self.statements.append(clear)
# Fix locations (line numbers/column offsets).
for stmt in self.statements:
@@ -961,20 +952,20 @@ class AssertionRewriter(ast.NodeVisitor):
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), astStr(target_id))
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), astStr(name.id))
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@@ -1005,14 +996,12 @@ class AssertionRewriter(ast.NodeVisitor):
]
):
pytest_temp = self.variable()
self.variables_overwrite[
v.left.target.id
] = v.left # type:ignore[assignment]
self.variables_overwrite[v.left.target.id] = pytest_temp
v.left.target.id = pytest_temp
self.push_format_context()
res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(astStr(expl))
expl_format = self.pop_format_context(ast.Str(expl))
call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call))
if i < levels:
@@ -1024,7 +1013,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.statements = body = inner
self.statements = save
self.expl_stmts = fail_save
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@@ -1048,19 +1037,10 @@ 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]
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[
keyword.value.id
] # type:ignore[assignment]
res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg:
@@ -1095,13 +1075,7 @@ class AssertionRewriter(ast.NodeVisitor):
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[
comp.left.id
] # type:ignore[assignment]
if isinstance(comp.left, namedExpr):
self.variables_overwrite[
comp.left.target.id
] = comp.left # type:ignore[assignment]
comp.left.id = self.variables_overwrite[comp.left.id]
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
@@ -1119,17 +1093,15 @@ class AssertionRewriter(ast.NodeVisitor):
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[
left_res.id
] = next_operand # type:ignore[assignment]
self.variables_overwrite[left_res.id] = next_operand.target.id
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})"
results.append(next_res)
sym = BINOP_MAP[op.__class__]
syms.append(astStr(sym))
syms.append(ast.Str(sym))
expl = f"{left_expl} {sym} {next_expl}"
expls.append(astStr(expl))
expls.append(ast.Str(expl))
res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl

View File

@@ -213,7 +213,7 @@ class LFPluginCollWrapper:
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, (Session, Package)):
if isinstance(collector, Session):
out = yield
res: CollectReport = out.get_result()

View File

@@ -241,7 +241,7 @@ class DontReadFromInput(TextIO):
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannot truncate stdin")
raise UnsupportedOperation("cannont truncate stdin")
def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin")

View File

@@ -526,10 +526,7 @@ class PytestPluginManager(PluginManager):
# Internal API for local conftest plugin handling.
#
def _set_initial_conftests(
self,
namespace: argparse.Namespace,
rootpath: Path,
testpaths_ini: Sequence[str],
self, namespace: argparse.Namespace, rootpath: Path
) -> None:
"""Load initial conftest files given a preparsed "namespace".
@@ -546,7 +543,7 @@ class PytestPluginManager(PluginManager):
)
self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir + testpaths_ini
testpaths = namespace.file_or_dir
foundanchor = False
for testpath in testpaths:
path = str(testpath)
@@ -555,14 +552,7 @@ class PytestPluginManager(PluginManager):
if i != -1:
path = path[:i]
anchor = absolutepath(current / path)
# Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169).
try:
anchor_exists = anchor.exists()
except OSError: # pragma: no cover
anchor_exists = False
if anchor_exists:
if anchor.exists(): # we found some file object
self._try_load_conftest(anchor, namespace.importmode, rootpath)
foundanchor = True
if not foundanchor:
@@ -1141,9 +1131,7 @@ class Config:
@hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
self.pluginmanager._set_initial_conftests(
early_config.known_args_namespace,
rootpath=early_config.rootpath,
testpaths_ini=self.getini("testpaths"),
early_config.known_args_namespace, rootpath=early_config.rootpath
)
def _initini(self, args: Sequence[str]) -> None:

View File

@@ -2,6 +2,7 @@ import io
import os
import sys
from typing import Generator
from typing import TextIO
import pytest
from _pytest.config import Config
@@ -10,7 +11,7 @@ from _pytest.nodes import Item
from _pytest.stash import StashKey
fault_handler_stderr_fd_key = StashKey[int]()
fault_handler_stderr_key = StashKey[TextIO]()
fault_handler_originally_enabled_key = StashKey[bool]()
@@ -25,9 +26,10 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None:
import faulthandler
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
stderr_fd_copy = os.dup(get_stderr_fileno())
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
def pytest_unconfigure(config: Config) -> None:
@@ -35,9 +37,9 @@ def pytest_unconfigure(config: Config) -> None:
faulthandler.disable()
# Close the dup file installed during pytest_configure.
if fault_handler_stderr_fd_key in config.stash:
os.close(config.stash[fault_handler_stderr_fd_key])
del config.stash[fault_handler_stderr_fd_key]
if fault_handler_stderr_key in config.stash:
config.stash[fault_handler_stderr_key].close()
del config.stash[fault_handler_stderr_key]
if config.stash.get(fault_handler_originally_enabled_key, False):
# Re-enable the faulthandler if it was originally enabled.
faulthandler.enable(file=get_stderr_fileno())
@@ -65,10 +67,10 @@ def get_timeout_config_value(config: Config) -> float:
@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
timeout = get_timeout_config_value(item.config)
if timeout > 0:
stderr = item.config.stash[fault_handler_stderr_key]
if timeout > 0 and stderr is not None:
import faulthandler
stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr)
try:
yield

View File

@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from typing_extensions import Literal
from _pytest._code.code import ExceptionRepr
from _pytest._code.code import ExceptionInfo
from _pytest.code import ExceptionInfo
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager

View File

@@ -302,7 +302,7 @@ def pytest_addoption(parser: Parser) -> None:
action="append",
default=[],
dest="logger_disable",
help="Disable a logger by name. Can be passed multiple times.",
help="Disable a logger by name. Can be passed multipe times.",
)

View File

@@ -18,7 +18,6 @@ import ast
import dataclasses
import enum
import re
import sys
import types
from typing import Callable
from typing import Iterator
@@ -27,11 +26,6 @@ from typing import NoReturn
from typing import Optional
from typing import Sequence
if sys.version_info >= (3, 8):
astNameConstant = ast.Constant
else:
astNameConstant = ast.NameConstant
__all__ = [
"Expression",
@@ -138,7 +132,7 @@ IDENT_PREFIX = "$"
def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF):
ret: ast.expr = astNameConstant(False)
ret: ast.expr = ast.NameConstant(False)
else:
ret = expr(s)
s.accept(TokenType.EOF, reject=True)

View File

@@ -7,7 +7,6 @@ from contextlib import contextmanager
from typing import Any
from typing import Generator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import Optional
from typing import overload
@@ -130,7 +129,7 @@ class MonkeyPatch:
def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = []
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
self._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None
@@ -291,13 +290,12 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval))
delattr(target, name)
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
"""Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset)))
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dic[name] = value # type: ignore[index]
dic[name] = value
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
"""Delete ``name`` from dict.
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
@@ -308,8 +306,7 @@ class MonkeyPatch:
raise KeyError(name)
else:
self._setitem.append((dic, name, dic.get(name, notset)))
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dic[name] # type: ignore[attr-defined]
del dic[name]
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
"""Set environment variable ``name`` to ``value``.
@@ -404,13 +401,11 @@ class MonkeyPatch:
for dictionary, key, value in reversed(self._setitem):
if value is notset:
try:
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dictionary[key] # type: ignore[attr-defined]
del dictionary[key]
except KeyError:
pass # Was already deleted, so we have the desired state.
else:
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dictionary[key] = value # type: ignore[index]
dictionary[key] = value
self._setitem[:] = []
if self._savesyspath is not None:
sys.path[:] = self._savesyspath

View File

@@ -6,7 +6,6 @@ import itertools
import os
import shutil
import sys
import types
import uuid
import warnings
from enum import Enum
@@ -29,8 +28,6 @@ from typing import Iterable
from typing import Iterator
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
@@ -66,33 +63,21 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
return path.joinpath(".lock")
def on_rm_rf_error(
func,
path: str,
excinfo: Union[
BaseException,
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
],
*,
start_path: Path,
) -> bool:
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
"""Handle known read-only errors during rmtree.
The returned value is used only by our own tests.
"""
if isinstance(excinfo, BaseException):
exc = excinfo
else:
exc = excinfo[1]
exctype, excvalue = exc[:2]
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
if isinstance(exc, FileNotFoundError):
if isinstance(excvalue, FileNotFoundError):
return False
if not isinstance(exc, PermissionError):
if not isinstance(excvalue, PermissionError):
warnings.warn(
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
)
return False
@@ -101,7 +86,7 @@ def on_rm_rf_error(
warnings.warn(
PytestWarning(
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
func, path, type(exc), exc
func, path, exctype, excvalue
)
)
)
@@ -164,10 +149,7 @@ def rm_rf(path: Path) -> None:
are read-only."""
path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path)
if sys.version_info >= (3, 12):
shutil.rmtree(str(path), onexc=onerror)
else:
shutil.rmtree(str(path), onerror=onerror)
shutil.rmtree(str(path), onerror=onerror)
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
@@ -353,7 +335,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
yield path
def cleanup_dead_symlinks(root: Path):
def cleanup_dead_symlink(root: Path):
for left_dir in root.iterdir():
if left_dir.is_symlink():
if not left_dir.resolve().exists():
@@ -371,7 +353,7 @@ def cleanup_numbered_dir(
for path in root.glob("garbage-*"):
try_cleanup(path, consider_lock_dead_if_created_before)
cleanup_dead_symlinks(root)
cleanup_dead_symlink(root)
def make_numbered_dir_with_cleanup(

View File

@@ -347,6 +347,10 @@ class TestReport(BaseReport):
elif isinstance(excinfo.value, skip.Exception):
outcome = "skipped"
r = excinfo._getreprcrash()
if r is None:
raise ValueError(
"There should always be a traceback entry for skipping a test."
)
if excinfo.value._use_item_location:
path, line = item.reportinfo()[:2]
assert line is not None

View File

@@ -28,7 +28,7 @@ from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf
from .pathlib import cleanup_dead_symlinks
from .pathlib import cleanup_dead_symlink
from _pytest.compat import final, get_user_id
from _pytest.config import Config
from _pytest.config import ExitCode
@@ -100,7 +100,7 @@ class TempPathFactory:
policy = config.getini("tmp_path_retention_policy")
if policy not in ("all", "failed", "none"):
raise ValueError(
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}."
)
return cls(
@@ -289,30 +289,31 @@ def tmp_path(
del request.node.stash[tmppath_result_key]
# remove dead symlink
basetemp = tmp_path_factory._basetemp
if basetemp is None:
return
cleanup_dead_symlink(basetemp)
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
"""After each session, remove base directory if all the tests passed,
the policy is "failed", and the basetemp is not specified by a user.
"""
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
basetemp = tmp_path_factory._basetemp
if basetemp is None:
if tmp_path_factory._basetemp is None:
return
policy = tmp_path_factory._retention_policy
if (
exitstatus == 0
and policy == "failed"
and tmp_path_factory._given_basetemp is None
):
if basetemp.is_dir():
passed_dir = tmp_path_factory._basetemp
if passed_dir.exists():
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
# permissions, etc, in which case we ignore it.
rmtree(basetemp, ignore_errors=True)
# Remove dead symlinks.
if basetemp.is_dir():
cleanup_dead_symlinks(basetemp)
rmtree(passed_dir, ignore_errors=True)
@hookimpl(tryfirst=True, hookwrapper=True)

View File

@@ -298,9 +298,6 @@ class TestCaseFunction(Function):
def stopTest(self, testcase: "unittest.TestCase") -> None:
pass
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
pass
def runtest(self) -> None:
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing

View File

@@ -149,7 +149,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
"""
Issue the warning :param:`message` for the definition of the given :param:`method`
this helps to log warnings for functions defined prior to finding an issue with them
this helps to log warnigns for functions defined prior to finding an issue with them
(like hook wrappers being marked in a legacy mechanism)
"""
lineno = method.__code__.co_firstlineno

View File

@@ -695,15 +695,11 @@ class TestInvocationVariants:
monkeypatch.chdir("world")
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
# pgk_resources has been deprecated entirely.
# While we could change the test to use implicit namespace packages, seems better
# to still ensure the old declaration via declare_namespace still works.
ignore_w = (
r"-Wignore:Deprecated call to `pkg_resources.declare_namespace",
r"-Wignore:pkg_resources is deprecated",
)
ignore_w = r"-Wignore:Deprecated call to `pkg_resources.declare_namespace"
result = pytester.runpytest(
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
)
assert result.ret == 0
result.stdout.fnmatch_lines(

View File

@@ -294,6 +294,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback
entry = tb.getcrashentry()
assert entry is not None
co = _pytest._code.Code.from_function(h)
assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 1
@@ -311,10 +312,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback
entry = tb.getcrashentry()
co = _pytest._code.Code.from_function(g)
assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 2
assert entry.frame.code.name == "g"
assert entry is None
def test_excinfo_exconly():
@@ -1575,20 +1573,3 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
pytest.importorskip("exceptiongroup")
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None:
"""Regression test for #10903.
We're not really sure what should be *displayed* here, so this test
just verified that at least it doesn't crash.
"""
pytester.makepyfile(
"""
def test():
__tracebackhide__ = True
1 / 0
"""
)
result = pytester.runpytest()
assert result.ret == 1

View File

@@ -897,29 +897,25 @@ class TestConftestCustomization:
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
"""Ensure we can collect files with weird file extensions as Python
modules (#2369)"""
# Implement a little meta path finder to import files containing
# We'll implement a little finder and loader to import files containing
# Python source code whose file extension is ".narf".
pytester.makeconftest(
"""
import sys
import os.path
from importlib.util import spec_from_loader
from importlib.machinery import SourceFileLoader
import sys, os, imp
from _pytest.python import Module
class MetaPathFinder:
def find_spec(self, fullname, path, target=None):
if os.path.exists(fullname + ".narf"):
return spec_from_loader(
fullname,
SourceFileLoader(fullname, fullname + ".narf"),
)
sys.meta_path.append(MetaPathFinder())
class Loader(object):
def load_module(self, name):
return imp.load_source(name, name + ".narf")
class Finder(object):
def find_module(self, name, path=None):
if os.path.exists(name + ".narf"):
return Loader()
sys.meta_path.append(Finder())
def pytest_collect_file(file_path, parent):
if file_path.suffix == ".narf":
return Module.from_parent(path=file_path, parent=parent)
"""
return Module.from_parent(path=file_path, parent=parent)"""
)
pytester.makefile(
".narf",

View File

@@ -1436,96 +1436,6 @@ class TestIssue10743:
assert result.ret == 0
@pytest.mark.skipif(
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
)
class TestIssue11028:
def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_in_string():
assert (obj := "foo") in obj
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_in_operand_json_dumps(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
import json
def test_json_encoder():
assert (obj := "foo") in json.dumps(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a):
return a
def test_call_other_function_arg():
assert (obj := "foo") == f(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function_keyword_arg(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a='test'):
return a
def test_call_other_function_k_arg():
assert (obj := "foo") == f(a=obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function_arg_as_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a='test'):
return a
def test_function_of_function():
assert (obj := "foo") == f(f(obj))
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_gt_operand_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def add_one(a):
return a + 1
def test_gt():
assert (obj := 4) > add_one(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
@pytest.mark.skipif(
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
)

View File

@@ -420,13 +420,7 @@ class TestLastFailed:
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 failed in*"])
@pytest.mark.parametrize("parent", ("session", "package"))
def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None:
if parent == "package":
pytester.makepyfile(
__init__="",
)
def test_terminal_report_lastfailed(self, pytester: Pytester) -> None:
test_a = pytester.makepyfile(
test_a="""
def test_a1(): pass

View File

@@ -1247,48 +1247,6 @@ def test_collect_pyargs_with_testpaths(
result.stdout.fnmatch_lines(["*1 passed in*"])
def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:
"""The testpaths ini option should load conftests in those paths as 'initial' (#10987)."""
p = pytester.mkdir("some_path")
p.joinpath("conftest.py").write_text(
textwrap.dedent(
"""
def pytest_sessionstart(session):
raise Exception("pytest_sessionstart hook successfully run")
"""
)
)
pytester.makeini(
"""
[pytest]
testpaths = some_path
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
"INTERNALERROR* Exception: pytest_sessionstart hook successfully run"
)
def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None:
"""Long option values do not break initial conftests handling (#10169)."""
option_value = "x" * 1024 * 1000
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addoption("--xx", default=None)
"""
)
pytester.makepyfile(
f"""
def test_foo(request):
assert request.config.getoption("xx") == {option_value!r}
"""
)
result = pytester.runpytest(f"--xx={option_value}")
assert result.ret == 0
def test_collect_symlink_file_arg(pytester: Pytester) -> None:
"""Collect a direct symlink works even if it does not match python_files (#4325)."""
real = pytester.makepyfile(

View File

@@ -35,7 +35,7 @@ def conftest_setinitial(
self.importmode = "prepend"
namespace = cast(argparse.Namespace, Namespace())
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[])
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]))
@pytest.mark.usefixtures("_sys_snapshot")

View File

@@ -425,7 +425,9 @@ def test_context_classmethod() -> None:
assert A.x == 1
@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning")
@pytest.mark.filterwarnings(
"ignore:Deprecated call to `pkg_resources.declare_namespace"
)
def test_syspath_prepend_with_namespace_packages(
pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:

View File

@@ -82,7 +82,7 @@ def test_no_ini(pytester: Pytester, file_structure) -> None:
def test_clean_up(pytester: Pytester) -> None:
"""Test that the plugin cleans up after itself."""
# This is tough to test behaviorally because the cleanup really runs last.
# This is tough to test behaviorly because the cleanup really runs last.
# So the test make several implementation assumptions:
# - Cleanup is done in pytest_unconfigure().
# - Not a hookwrapper.

View File

@@ -512,20 +512,20 @@ class TestRmRf:
# unknown exception
with pytest.warns(pytest.PytestWarning):
exc_info1 = (RuntimeError, RuntimeError(), None)
exc_info1 = (None, RuntimeError(), None)
on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path)
assert fn.is_file()
# we ignore FileNotFoundError
exc_info2 = (FileNotFoundError, FileNotFoundError(), None)
exc_info2 = (None, FileNotFoundError(), None)
assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path)
# unknown function
with pytest.warns(
pytest.PytestWarning,
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\n<class 'PermissionError'>: ",
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ",
):
exc_info3 = (PermissionError, PermissionError(), None)
exc_info3 = (None, PermissionError(), None)
on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path)
assert fn.is_file()
@@ -533,12 +533,12 @@ class TestRmRf:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
with pytest.warns(None) as warninfo: # type: ignore[call-overload]
exc_info4 = PermissionError()
exc_info4 = (None, PermissionError(), None)
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
assert fn.is_file()
assert not [x.message for x in warninfo]
exc_info5 = PermissionError()
exc_info5 = (None, PermissionError(), None)
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
assert not fn.is_file()

View File

@@ -0,0 +1,25 @@
def test_tbh_chained(testdir):
"""Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904)."""
p = testdir.makepyfile(
"""
import pytest
def f1():
__tracebackhide__ = True
try:
return f1.meh
except AttributeError:
pytest.fail("fail")
@pytest.fixture
def fix():
f1()
def test(fix):
pass
"""
)
result = testdir.runpytest(str(p))
assert "'function' object has no attribute 'meh'" not in result.stdout.str()
assert result.ret == 1

View File

@@ -9,7 +9,6 @@ from typing import Optional
from typing_extensions import assert_type
import pytest
from pytest import MonkeyPatch
# Issue #7488.
@@ -30,19 +29,6 @@ def check_parametrize_ids_callable(func) -> None:
pass
# Issue #10999.
def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None:
from typing import TypedDict
class Foo(TypedDict):
x: int
y: float
a: Foo = {"x": 1, "y": 3.14}
monkeypatch.setitem(a, "x", 2)
monkeypatch.delitem(a, "y")
def check_raises_is_a_context_manager(val: bool) -> None:
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
pass