diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cd1ffdbf9d4..cf5027223e1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -43,6 +43,7 @@ jobs:
"windows-py39",
"windows-py310",
"windows-py311",
+ "windows-py312",
"ubuntu-py37",
"ubuntu-py37-pluggy",
@@ -51,12 +52,13 @@ jobs:
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
+ "ubuntu-py312",
"ubuntu-pypy3",
"macos-py37",
- "macos-py38",
"macos-py39",
"macos-py310",
+ "macos-py312",
"docs",
"doctesting",
@@ -86,9 +88,13 @@ jobs:
os: windows-latest
tox_env: "py310-xdist"
- name: "windows-py311"
- python: "3.11-dev"
+ python: "3.11"
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"
@@ -116,10 +122,15 @@ jobs:
os: ubuntu-latest
tox_env: "py310-xdist"
- name: "ubuntu-py311"
- python: "3.11-dev"
+ python: "3.11"
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
@@ -129,19 +140,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"
@@ -168,6 +179,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
+ check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies
run: |
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d672875962f..d83b2cddcdb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -52,7 +52,7 @@ repos:
rev: v2.2.0
hooks:
- id: setup-cfg-fmt
- args: ["--max-py-version=3.11", "--include-version-classifiers"]
+ args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
diff --git a/AUTHORS b/AUTHORS
index a4c7f856884..2c1f02fcbcd 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -8,6 +8,7 @@ Abdeali JK
Abdelrahman Elbehery
Abhijeet Kasurde
Adam Johnson
+Adam Stewart
Adam Uhlir
Ahn Ki-Wook
Akiomi Kamakura
diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst
index e3919f88ea6..bcc0669a62f 100644
--- a/doc/en/announce/index.rst
+++ b/doc/en/announce/index.rst
@@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2
+ release-7.3.2
release-7.3.1
release-7.3.0
release-7.2.2
diff --git a/doc/en/announce/release-7.3.2.rst b/doc/en/announce/release-7.3.2.rst
new file mode 100644
index 00000000000..b3b112f0d8e
--- /dev/null
+++ b/doc/en/announce/release-7.3.2.rst
@@ -0,0 +1,21 @@
+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
diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst
index 64bcbf5bd49..ea0c6a71a28 100644
--- a/doc/en/backwards-compatibility.rst
+++ b/doc/en/backwards-compatibility.rst
@@ -92,3 +92,5 @@ pytest version min. Python version
5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+
============== ===================
+
+`Status of Python Versions `__.
diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst
index 7e9b51d002d..7ae185f6c7d 100644
--- a/doc/en/builtin.rst
+++ b/doc/en/builtin.rst
@@ -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:29
+ monkeypatch -- .../_pytest/monkeypatch.py:30
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst
index c13c05936d5..cdc9e66f68b 100644
--- a/doc/en/changelog.rst
+++ b/doc/en/changelog.rst
@@ -28,6 +28,30 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
+pytest 7.3.2 (2023-06-10)
+=========================
+
+Bug Fixes
+---------
+
+- `#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 `_: Support for Python 3.12 (beta at the time of writing).
+
+
+- `#10987 `_: :confval:`testpaths` is now honored to load root ``conftests``.
+
+
+- `#10999 `_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
+
+
+- `#11028 `_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
+
+
+- `#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)
=========================
@@ -567,7 +591,7 @@ Breaking Changes
- `#7259 `_: The :ref:`Node.reportinfo() ` 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 a `os.PathLike[str]`, these plugins are unaffacted.
+ Since `py.path.local` is an `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.
@@ -4067,7 +4091,7 @@ Removals
See our :ref:`docs ` on information on how to update your code.
-- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
+- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
Use ``Node.get_closest_marker(name)`` as a replacement.
diff --git a/doc/en/conf.py b/doc/en/conf.py
index 5184ee7b1e5..32f508219a6 100644
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -341,7 +341,7 @@
# The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = ''
-# The unique identifier of the text. This can be a ISBN number
+# The unique identifier of the text. This can be an ISBN number
# or the project homepage.
# epub_identifier = ''
diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py
index bc39a1f6b20..dd1ebe88d7e 100644
--- a/doc/en/example/nonpython/conftest.py
+++ b/doc/en/example/nonpython/conftest.py
@@ -38,6 +38,7 @@ def repr_failure(self, excinfo):
" no further details known at this point.",
]
)
+ return super().repr_failure(excinfo)
def reportinfo(self):
return self.path, 0, f"usecase: {self.name}"
diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst
index f4157114153..2dbf7d3870c 100644
--- a/doc/en/getting-started.rst
+++ b/doc/en/getting-started.rst
@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
- pytest 7.3.1
+ pytest 7.3.2
.. _`simpletest`:
diff --git a/doc/en/index.rst b/doc/en/index.rst
index 6f3115b19c2..87213878890 100644
--- a/doc/en/index.rst
+++ b/doc/en/index.rst
@@ -1,11 +1,10 @@
:orphan:
-..
- .. sidebar:: Next Open Trainings
+.. sidebar:: Next Open Trainings
- - `Professional Testing with Python `_, via `Python Academy `_, March 7th to 9th 2023 (3 day in-depth training), Remote
+ - `Professional Testing with Python `_, via `Python Academy `_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
- Also see :doc:`previous talks and blogposts `.
+ Also see :doc:`previous talks and blogposts `.
.. _features:
diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst
index 963e666ade3..48fbe2734de 100644
--- a/doc/en/reference/reference.rst
+++ b/doc/en/reference/reference.rst
@@ -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. Alterative to ``BUILD_NUMBER`` variable.
+When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
.. envvar:: BUILD_NUMBER
-When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
+When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
.. envvar:: PYTEST_ADDOPTS
@@ -1713,13 +1713,12 @@ 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 ` 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.
@@ -1728,8 +1727,17 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
testpaths = testing doc
- This tells pytest to only look for tests in ``testing`` and ``doc``
- directories when executing from the root directory.
+ This configuration means that executing:
+
+ .. code-block:: console
+
+ pytest
+
+ has the same practical effects as executing:
+
+ .. code-block:: console
+
+ pytest testing doc
.. confval:: tmp_path_retention_count
@@ -1744,7 +1752,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
@@ -1763,7 +1771,7 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
tmp_path_retention_policy = "all"
- Default: all
+ Default: ``all``
.. confval:: usefixtures
@@ -1996,7 +2004,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 multipe
+ Disable a logger by name. Can be passed multiple
times.
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
diff --git a/setup.cfg b/setup.cfg
index 56dadae7bf5..b7b74cd95d3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,6 +22,7 @@ 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
@@ -73,6 +74,7 @@ testing =
nose
pygments>=2.7.2
requests
+ setuptools
xmlschema
[options.package_data]
diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py
index fb64830f814..73a070d19a8 100644
--- a/src/_pytest/_py/path.py
+++ b/src/_pytest/_py/path.py
@@ -953,7 +953,7 @@ def ensure(self, *args, **kwargs):
else:
p.dirpath()._ensuredirs()
if not p.check(file=1):
- p.open("w").close()
+ p.open("wb").close()
return p
@overload
diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py
index 8b182347052..ab83fee32b2 100644
--- a/src/_pytest/assertion/rewrite.py
+++ b/src/_pytest/assertion/rewrite.py
@@ -46,8 +46,14 @@
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"]()
@@ -680,9 +686,12 @@ def run(self, mod: ast.Module) -> None:
if (
expect_docstring
and isinstance(item, ast.Expr)
- and isinstance(item.value, ast.Str)
+ and isinstance(item.value, astStr)
):
- doc = item.value.s
+ if sys.version_info >= (3, 8):
+ doc = item.value.value
+ else:
+ doc = item.value.s
if self.is_rewrite_disabled(doc):
return
expect_docstring = False
@@ -814,7 +823,7 @@ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
current = self.stack.pop()
if self.stack:
self.explanation_specifiers = self.stack[-1]
- keys = [ast.Str(key) for key in current.keys()]
+ keys = [astStr(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))
@@ -868,16 +877,16 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
- msg = self.pop_format_context(ast.Str(explanation))
+ msg = self.pop_format_context(astStr(explanation))
# Failed
if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert "
else:
- assertmsg = ast.Str("")
+ assertmsg = astStr("")
gluestr = "assert "
- err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
+ err_explanation = ast.BinOp(astStr(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)
@@ -893,8 +902,8 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
hook_call_pass = ast.Expr(
self.helper(
"_call_assertion_pass",
- ast.Num(assert_.lineno),
- ast.Str(orig),
+ astNum(assert_.lineno),
+ astStr(orig),
fmt_pass,
)
)
@@ -913,7 +922,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
variables = [
ast.Name(name, ast.Store()) for name in self.format_variables
]
- clear_format = ast.Assign(variables, ast.NameConstant(None))
+ clear_format = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear_format)
else: # Original assertion rewriting
@@ -924,9 +933,9 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation
else:
- assertmsg = ast.Str("")
+ assertmsg = astStr("")
explanation = "assert " + explanation
- template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
+ template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load())
@@ -938,7 +947,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
# 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, ast.NameConstant(None))
+ clear = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear)
# Fix locations (line numbers/column offsets).
for stmt in self.statements:
@@ -952,20 +961,20 @@ def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined]
- inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
+ inlocs = ast.Compare(astStr(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), ast.Str(target_id))
+ expr = ast.IfExp(test, self.display(name), astStr(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(ast.Str(name.id), [ast.In()], [locs])
+ inlocs = ast.Compare(astStr(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), ast.Str(name.id))
+ expr = ast.IfExp(test, self.display(name), astStr(name.id))
return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@@ -996,12 +1005,14 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
]
):
pytest_temp = self.variable()
- self.variables_overwrite[v.left.target.id] = pytest_temp
+ self.variables_overwrite[
+ v.left.target.id
+ ] = v.left # type:ignore[assignment]
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(ast.Str(expl))
+ expl_format = self.pop_format_context(astStr(expl))
call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call))
if i < levels:
@@ -1013,7 +1024,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
self.statements = body = inner
self.statements = save
self.expl_stmts = fail_save
- expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
+ expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@@ -1037,10 +1048,19 @@ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
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:
@@ -1075,7 +1095,13 @@ 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.id = self.variables_overwrite[comp.left.id]
+ 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]
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
@@ -1093,15 +1119,17 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
- self.variables_overwrite[left_res.id] = next_operand.target.id
+ self.variables_overwrite[
+ left_res.id
+ ] = next_operand # type:ignore[assignment]
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(ast.Str(sym))
+ syms.append(astStr(sym))
expl = f"{left_expl} {sym} {next_expl}"
- expls.append(ast.Str(expl))
+ expls.append(astStr(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
diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py
index 719b32f7e0e..940dc5d8782 100755
--- a/src/_pytest/cacheprovider.py
+++ b/src/_pytest/cacheprovider.py
@@ -213,7 +213,7 @@ def __init__(self, lfplugin: "LFPlugin") -> None:
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector):
- if isinstance(collector, Session):
+ if isinstance(collector, (Session, Package)):
out = yield
res: CollectReport = out.get_result()
diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py
index 275322cc335..a8ca0869f33 100644
--- a/src/_pytest/capture.py
+++ b/src/_pytest/capture.py
@@ -241,7 +241,7 @@ def tell(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: Optional[int] = None) -> int:
- raise UnsupportedOperation("cannont truncate stdin")
+ raise UnsupportedOperation("cannot truncate stdin")
def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin")
diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py
index 720f3953153..74905ff4c8e 100644
--- a/src/_pytest/config/__init__.py
+++ b/src/_pytest/config/__init__.py
@@ -526,7 +526,10 @@ def pytest_configure(self, config: "Config") -> None:
# Internal API for local conftest plugin handling.
#
def _set_initial_conftests(
- self, namespace: argparse.Namespace, rootpath: Path
+ self,
+ namespace: argparse.Namespace,
+ rootpath: Path,
+ testpaths_ini: Sequence[str],
) -> None:
"""Load initial conftest files given a preparsed "namespace".
@@ -543,7 +546,7 @@ def _set_initial_conftests(
)
self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs
- testpaths = namespace.file_or_dir
+ testpaths = namespace.file_or_dir + testpaths_ini
foundanchor = False
for testpath in testpaths:
path = str(testpath)
@@ -552,7 +555,14 @@ def _set_initial_conftests(
if i != -1:
path = path[:i]
anchor = absolutepath(current / path)
- if anchor.exists(): # we found some file object
+
+ # 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:
self._try_load_conftest(anchor, namespace.importmode, rootpath)
foundanchor = True
if not foundanchor:
@@ -1131,7 +1141,9 @@ def _processopt(self, opt: "Argument") -> None:
@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
+ early_config.known_args_namespace,
+ rootpath=early_config.rootpath,
+ testpaths_ini=self.getini("testpaths"),
)
def _initini(self, args: Sequence[str]) -> None:
diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py
index b9c925582ca..af879aa44cf 100644
--- a/src/_pytest/faulthandler.py
+++ b/src/_pytest/faulthandler.py
@@ -2,7 +2,6 @@
import os
import sys
from typing import Generator
-from typing import TextIO
import pytest
from _pytest.config import Config
@@ -11,7 +10,7 @@
from _pytest.stash import StashKey
-fault_handler_stderr_key = StashKey[TextIO]()
+fault_handler_stderr_fd_key = StashKey[int]()
fault_handler_originally_enabled_key = StashKey[bool]()
@@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None:
import faulthandler
- stderr_fd_copy = os.dup(get_stderr_fileno())
- config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
+ config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
- faulthandler.enable(file=config.stash[fault_handler_stderr_key])
+ faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
def pytest_unconfigure(config: Config) -> None:
@@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
faulthandler.disable()
# Close the dup file installed during pytest_configure.
- if fault_handler_stderr_key in config.stash:
- config.stash[fault_handler_stderr_key].close()
- del config.stash[fault_handler_stderr_key]
+ 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 config.stash.get(fault_handler_originally_enabled_key, False):
# Re-enable the faulthandler if it was originally enabled.
faulthandler.enable(file=get_stderr_fileno())
@@ -67,10 +65,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)
- stderr = item.config.stash[fault_handler_stderr_key]
- if timeout > 0 and stderr is not None:
+ if timeout > 0:
import faulthandler
+ stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr)
try:
yield
diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py
index 143ec190c2d..95b4265ea2f 100644
--- a/src/_pytest/hookspec.py
+++ b/src/_pytest/hookspec.py
@@ -21,7 +21,7 @@
from typing_extensions import Literal
from _pytest._code.code import ExceptionRepr
- from _pytest.code import ExceptionInfo
+ from _pytest._code.code import ExceptionInfo
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager
diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py
index 4e3d12475d1..2480a5edd29 100644
--- a/src/_pytest/logging.py
+++ b/src/_pytest/logging.py
@@ -302,7 +302,7 @@ def add_option_ini(option, dest, default=None, type=None, **kwargs):
action="append",
default=[],
dest="logger_disable",
- help="Disable a logger by name. Can be passed multipe times.",
+ help="Disable a logger by name. Can be passed multiple times.",
)
diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py
index f82a81d44c5..9287bcee50c 100644
--- a/src/_pytest/mark/expression.py
+++ b/src/_pytest/mark/expression.py
@@ -18,6 +18,7 @@
import dataclasses
import enum
import re
+import sys
import types
from typing import Callable
from typing import Iterator
@@ -26,6 +27,11 @@
from typing import Optional
from typing import Sequence
+if sys.version_info >= (3, 8):
+ astNameConstant = ast.Constant
+else:
+ astNameConstant = ast.NameConstant
+
__all__ = [
"Expression",
@@ -132,7 +138,7 @@ def reject(self, expected: Sequence[TokenType]) -> NoReturn:
def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF):
- ret: ast.expr = ast.NameConstant(False)
+ ret: ast.expr = astNameConstant(False)
else:
ret = expr(s)
s.accept(TokenType.EOF, reject=True)
diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py
index c6e29ac7642..9e51ff33538 100644
--- a/src/_pytest/monkeypatch.py
+++ b/src/_pytest/monkeypatch.py
@@ -7,6 +7,7 @@
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
@@ -129,7 +130,7 @@ class MonkeyPatch:
def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = []
- self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
+ self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
self._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None
@@ -290,12 +291,13 @@ def delattr(
self._setattr.append((target, name, oldval))
delattr(target, name)
- def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
+ def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
"""Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset)))
- dic[name] = value
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ dic[name] = value # type: ignore[index]
- def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
+ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
"""Delete ``name`` from dict.
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
@@ -306,7 +308,8 @@ def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> N
raise KeyError(name)
else:
self._setitem.append((dic, name, dic.get(name, notset)))
- del dic[name]
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ del dic[name] # type: ignore[attr-defined]
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
"""Set environment variable ``name`` to ``value``.
@@ -401,11 +404,13 @@ def undo(self) -> None:
for dictionary, key, value in reversed(self._setitem):
if value is notset:
try:
- del dictionary[key]
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ del dictionary[key] # type: ignore[attr-defined]
except KeyError:
pass # Was already deleted, so we have the desired state.
else:
- dictionary[key] = value
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ dictionary[key] = value # type: ignore[index]
self._setitem[:] = []
if self._savesyspath is not None:
sys.path[:] = self._savesyspath
diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py
index d7f5ab9b459..3cc2bace55b 100644
--- a/src/_pytest/tmpdir.py
+++ b/src/_pytest/tmpdir.py
@@ -100,7 +100,7 @@ def from_config(
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 intput: {policy}."
+ f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
)
return cls(
diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py
index 86fa9a07e0c..bd5f4187343 100644
--- a/src/_pytest/warning_types.py
+++ b/src/_pytest/warning_types.py
@@ -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 warnigns for functions defined prior to finding an issue with them
+ this helps to log warnings for functions defined prior to finding an issue with them
(like hook wrappers being marked in a legacy mechanism)
"""
lineno = method.__code__.co_firstlineno
diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py
index 6b421dde641..7616f1419d6 100644
--- a/testing/acceptance_test.py
+++ b/testing/acceptance_test.py
@@ -695,11 +695,15 @@ def test_cmdline_python_namespace_package(
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"
+ ignore_w = (
+ r"-Wignore:Deprecated call to `pkg_resources.declare_namespace",
+ r"-Wignore:pkg_resources is deprecated",
+ )
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(
diff --git a/testing/python/collect.py b/testing/python/collect.py
index ac3edd395ab..52b34800965 100644
--- a/testing/python/collect.py
+++ b/testing/python/collect.py
@@ -897,25 +897,29 @@ def pytest_pycollect_makeitem(collector, name, obj):
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
"""Ensure we can collect files with weird file extensions as Python
modules (#2369)"""
- # We'll implement a little finder and loader to import files containing
+ # Implement a little meta path finder to import files containing
# Python source code whose file extension is ".narf".
pytester.makeconftest(
"""
- import sys, os, imp
+ import sys
+ import os.path
+ from importlib.util import spec_from_loader
+ from importlib.machinery import SourceFileLoader
from _pytest.python import Module
- 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())
+ 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())
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",
diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py
index 8d944140307..245241af2b2 100644
--- a/testing/test_assertrewrite.py
+++ b/testing/test_assertrewrite.py
@@ -1436,6 +1436,96 @@ def test_walrus_operator_not_override_value():
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"
)
diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py
index 2f8517f9962..f870d7b162e 100644
--- a/testing/test_cacheprovider.py
+++ b/testing/test_cacheprovider.py
@@ -420,7 +420,13 @@ def test_fail(val):
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 failed in*"])
- def test_terminal_report_lastfailed(self, pytester: Pytester) -> None:
+ @pytest.mark.parametrize("parent", ("session", "package"))
+ def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None:
+ if parent == "package":
+ pytester.makepyfile(
+ __init__="",
+ )
+
test_a = pytester.makepyfile(
test_a="""
def test_a1(): pass
diff --git a/testing/test_collection.py b/testing/test_collection.py
index d907244d551..bbcb358b6ff 100644
--- a/testing/test_collection.py
+++ b/testing/test_collection.py
@@ -1247,6 +1247,48 @@ 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(
diff --git a/testing/test_conftest.py b/testing/test_conftest.py
index d2bf860c6fe..d6abca5368f 100644
--- a/testing/test_conftest.py
+++ b/testing/test_conftest.py
@@ -35,7 +35,7 @@ def __init__(self) -> None:
self.importmode = "prepend"
namespace = cast(argparse.Namespace, Namespace())
- conftest._set_initial_conftests(namespace, rootpath=Path(args[0]))
+ conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[])
@pytest.mark.usefixtures("_sys_snapshot")
diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py
index 3d09ef4263a..500e244531a 100644
--- a/testing/test_monkeypatch.py
+++ b/testing/test_monkeypatch.py
@@ -425,9 +425,7 @@ class A:
assert A.x == 1
-@pytest.mark.filterwarnings(
- "ignore:Deprecated call to `pkg_resources.declare_namespace"
-)
+@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning")
def test_syspath_prepend_with_namespace_packages(
pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
diff --git a/testing/test_python_path.py b/testing/test_python_path.py
index 5ee0f55e36a..e1628feb159 100644
--- a/testing/test_python_path.py
+++ b/testing/test_python_path.py
@@ -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 behaviorly because the cleanup really runs last.
+ # This is tough to test behaviorally because the cleanup really runs last.
# So the test make several implementation assumptions:
# - Cleanup is done in pytest_unconfigure().
# - Not a hookwrapper.
diff --git a/testing/typing_checks.py b/testing/typing_checks.py
index d15b3988bb5..57f2bae475f 100644
--- a/testing/typing_checks.py
+++ b/testing/typing_checks.py
@@ -9,6 +9,7 @@
from typing_extensions import assert_type
import pytest
+from pytest import MonkeyPatch
# Issue #7488.
@@ -29,6 +30,19 @@ 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