Merge branch 'dict-list-assert-trunc' of https://github.com/mberkowitz03/pytest into dict-list-assert-trunc
This commit is contained in:
commit
bf911a168e
|
@ -26,7 +26,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.0.2
|
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
days-before-issue-close: 7
|
days-before-issue-close: 7
|
||||||
only-labels: "status: needs information"
|
only-labels: "status: needs information"
|
||||||
stale-issue-label: "stale"
|
stale-issue-label: "stale"
|
||||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
stale-issue-message: "This issue is stale because it has the `status: needs information` label and requested follow-up information was not provided for 14 days."
|
||||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
close-issue-message: "This issue was closed because it has the `status: needs information` label and follow-up information has not been provided for 7 days since being marked as stale."
|
||||||
days-before-pr-stale: -1
|
days-before-pr-stale: -1
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: -1
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.0.2
|
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [package]
|
needs: [package]
|
||||||
|
|
|
@ -46,7 +46,7 @@ jobs:
|
||||||
run: python scripts/update-plugin-list.py
|
run: python scripts/update-plugin-list.py
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e
|
uses: peter-evans/create-pull-request@c55203cfde3e5c11a452d352b4393e68b85b4533
|
||||||
with:
|
with:
|
||||||
commit-message: '[automated] Update plugin list'
|
commit-message: '[automated] Update plugin list'
|
||||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: "v0.3.4"
|
rev: "v0.3.7"
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--fix"]
|
args: ["--fix"]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -137,6 +137,7 @@ Endre Galaczi
|
||||||
Eric Hunsberger
|
Eric Hunsberger
|
||||||
Eric Liu
|
Eric Liu
|
||||||
Eric Siegerman
|
Eric Siegerman
|
||||||
|
Eric Yuan
|
||||||
Erik Aronesty
|
Erik Aronesty
|
||||||
Erik Hasse
|
Erik Hasse
|
||||||
Erik M. Bray
|
Erik M. Bray
|
||||||
|
@ -432,6 +433,7 @@ Xixi Zhao
|
||||||
Xuan Luong
|
Xuan Luong
|
||||||
Xuecong Liao
|
Xuecong Liao
|
||||||
Yannick Péroux
|
Yannick Péroux
|
||||||
|
Yao Xiao
|
||||||
Yoav Caspi
|
Yoav Caspi
|
||||||
Yuliang Shao
|
Yuliang Shao
|
||||||
Yusuke Kadowaki
|
Yusuke Kadowaki
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given.
|
|
@ -0,0 +1 @@
|
||||||
|
Improve namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs).
|
|
@ -0,0 +1 @@
|
||||||
|
cache: create cache directory supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics.
|
|
@ -224,6 +224,7 @@ place the objects you want to appear in the doctest namespace:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
# content of conftest.py
|
# content of conftest.py
|
||||||
|
import pytest
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1279,8 +1279,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
|
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
|
||||||
when collecting Python modules. Default is ``False``.
|
when collecting Python modules. Default is ``False``.
|
||||||
|
|
||||||
Set to ``True`` if you are testing namespace packages installed into a virtual environment and it is important for
|
Set to ``True`` if the package you are testing is part of a namespace package.
|
||||||
your packages to be imported using their full namespace package name.
|
|
||||||
|
|
||||||
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
|
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
|
||||||
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
|
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
|
||||||
|
|
|
@ -7,6 +7,7 @@ import dataclasses
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
@ -123,6 +124,10 @@ class Cache:
|
||||||
stacklevel=3,
|
stacklevel=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _mkdir(self, path: Path) -> None:
|
||||||
|
self._ensure_cache_dir_and_supporting_files()
|
||||||
|
path.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
def mkdir(self, name: str) -> Path:
|
def mkdir(self, name: str) -> Path:
|
||||||
"""Return a directory path object with the given name.
|
"""Return a directory path object with the given name.
|
||||||
|
|
||||||
|
@ -141,7 +146,7 @@ class Cache:
|
||||||
if len(path.parts) > 1:
|
if len(path.parts) > 1:
|
||||||
raise ValueError("name is not allowed to contain path separators")
|
raise ValueError("name is not allowed to contain path separators")
|
||||||
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
|
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
|
||||||
res.mkdir(exist_ok=True, parents=True)
|
self._mkdir(res)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _getvaluepath(self, key: str) -> Path:
|
def _getvaluepath(self, key: str) -> Path:
|
||||||
|
@ -178,19 +183,13 @@ class Cache:
|
||||||
"""
|
"""
|
||||||
path = self._getvaluepath(key)
|
path = self._getvaluepath(key)
|
||||||
try:
|
try:
|
||||||
if path.parent.is_dir():
|
self._mkdir(path.parent)
|
||||||
cache_dir_exists_already = True
|
|
||||||
else:
|
|
||||||
cache_dir_exists_already = self._cachedir.exists()
|
|
||||||
path.parent.mkdir(exist_ok=True, parents=True)
|
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
self.warn(
|
self.warn(
|
||||||
f"could not create cache path {path}: {exc}",
|
f"could not create cache path {path}: {exc}",
|
||||||
_ispytest=True,
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if not cache_dir_exists_already:
|
|
||||||
self._ensure_supporting_files()
|
|
||||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||||
try:
|
try:
|
||||||
f = path.open("w", encoding="UTF-8")
|
f = path.open("w", encoding="UTF-8")
|
||||||
|
@ -203,17 +202,32 @@ class Cache:
|
||||||
with f:
|
with f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
def _ensure_supporting_files(self) -> None:
|
def _ensure_cache_dir_and_supporting_files(self) -> None:
|
||||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
"""Create the cache dir and its supporting files."""
|
||||||
readme_path = self._cachedir / "README.md"
|
if self._cachedir.is_dir():
|
||||||
readme_path.write_text(README_CONTENT, encoding="UTF-8")
|
return
|
||||||
|
|
||||||
gitignore_path = self._cachedir.joinpath(".gitignore")
|
self._cachedir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
msg = "# Created by pytest automatically.\n*\n"
|
with tempfile.TemporaryDirectory(
|
||||||
gitignore_path.write_text(msg, encoding="UTF-8")
|
prefix="pytest-cache-files-",
|
||||||
|
dir=self._cachedir.parent,
|
||||||
|
) as newpath:
|
||||||
|
path = Path(newpath)
|
||||||
|
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
|
||||||
|
f.write(README_CONTENT)
|
||||||
|
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
|
||||||
|
f.write("# Created by pytest automatically.\n*\n")
|
||||||
|
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
|
||||||
|
f.write(CACHEDIR_TAG_CONTENT)
|
||||||
|
|
||||||
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
|
path.rename(self._cachedir)
|
||||||
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
|
# Create a directory in place of the one we just moved so that `TemporaryDirectory`'s
|
||||||
|
# cleanup doesn't complain.
|
||||||
|
#
|
||||||
|
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See
|
||||||
|
# https://github.com/python/cpython/issues/74168. Note that passing delete=False would
|
||||||
|
# do the wrong thing in case of errors and isn't supported until python 3.12.
|
||||||
|
path.mkdir()
|
||||||
|
|
||||||
|
|
||||||
class LFPluginCollWrapper:
|
class LFPluginCollWrapper:
|
||||||
|
|
|
@ -51,6 +51,7 @@ from _pytest.compat import NotSetType
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
from _pytest.config import _PluggyPlugin
|
from _pytest.config import _PluggyPlugin
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.deprecated import MARKED_FIXTURE
|
from _pytest.deprecated import MARKED_FIXTURE
|
||||||
|
@ -1365,6 +1366,33 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
default=[],
|
default=[],
|
||||||
help="List of default fixtures to be used with this project",
|
help="List of default fixtures to be used with this project",
|
||||||
)
|
)
|
||||||
|
group = parser.getgroup("general")
|
||||||
|
group.addoption(
|
||||||
|
"--fixtures",
|
||||||
|
"--funcargs",
|
||||||
|
action="store_true",
|
||||||
|
dest="showfixtures",
|
||||||
|
default=False,
|
||||||
|
help="Show available fixtures, sorted by plugin appearance "
|
||||||
|
"(fixtures with leading '_' are only shown with '-v')",
|
||||||
|
)
|
||||||
|
group.addoption(
|
||||||
|
"--fixtures-per-test",
|
||||||
|
action="store_true",
|
||||||
|
dest="show_fixtures_per_test",
|
||||||
|
default=False,
|
||||||
|
help="Show fixtures per test",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||||
|
if config.option.showfixtures:
|
||||||
|
showfixtures(config)
|
||||||
|
return 0
|
||||||
|
if config.option.show_fixtures_per_test:
|
||||||
|
show_fixtures_per_test(config)
|
||||||
|
return 0
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
|
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
|
||||||
|
@ -1761,3 +1789,137 @@ class FixtureManager:
|
||||||
for fixturedef in fixturedefs:
|
for fixturedef in fixturedefs:
|
||||||
if fixturedef.baseid in parentnodeids:
|
if fixturedef.baseid in parentnodeids:
|
||||||
yield fixturedef
|
yield fixturedef
|
||||||
|
|
||||||
|
|
||||||
|
def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]:
|
||||||
|
from _pytest.main import wrap_session
|
||||||
|
|
||||||
|
return wrap_session(config, _show_fixtures_per_test)
|
||||||
|
|
||||||
|
|
||||||
|
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
||||||
|
loc = Path(getlocation(func, invocation_dir))
|
||||||
|
prefix = Path("...", "_pytest")
|
||||||
|
try:
|
||||||
|
return str(prefix / loc.relative_to(_PYTEST_DIR))
|
||||||
|
except ValueError:
|
||||||
|
return bestrelpath(invocation_dir, loc)
|
||||||
|
|
||||||
|
|
||||||
|
def _show_fixtures_per_test(config: Config, session: "Session") -> None:
|
||||||
|
import _pytest.config
|
||||||
|
|
||||||
|
session.perform_collect()
|
||||||
|
invocation_dir = config.invocation_params.dir
|
||||||
|
tw = _pytest.config.create_terminal_writer(config)
|
||||||
|
verbose = config.getvalue("verbose")
|
||||||
|
|
||||||
|
def get_best_relpath(func) -> str:
|
||||||
|
loc = getlocation(func, invocation_dir)
|
||||||
|
return bestrelpath(invocation_dir, Path(loc))
|
||||||
|
|
||||||
|
def write_fixture(fixture_def: FixtureDef[object]) -> None:
|
||||||
|
argname = fixture_def.argname
|
||||||
|
if verbose <= 0 and argname.startswith("_"):
|
||||||
|
return
|
||||||
|
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
|
||||||
|
tw.write(f"{argname}", green=True)
|
||||||
|
tw.write(f" -- {prettypath}", yellow=True)
|
||||||
|
tw.write("\n")
|
||||||
|
fixture_doc = inspect.getdoc(fixture_def.func)
|
||||||
|
if fixture_doc:
|
||||||
|
write_docstring(
|
||||||
|
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tw.line(" no docstring available", red=True)
|
||||||
|
|
||||||
|
def write_item(item: nodes.Item) -> None:
|
||||||
|
# Not all items have _fixtureinfo attribute.
|
||||||
|
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
||||||
|
if info is None or not info.name2fixturedefs:
|
||||||
|
# This test item does not use any fixtures.
|
||||||
|
return
|
||||||
|
tw.line()
|
||||||
|
tw.sep("-", f"fixtures used by {item.name}")
|
||||||
|
# TODO: Fix this type ignore.
|
||||||
|
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
||||||
|
# dict key not used in loop but needed for sorting.
|
||||||
|
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
||||||
|
assert fixturedefs is not None
|
||||||
|
if not fixturedefs:
|
||||||
|
continue
|
||||||
|
# Last item is expected to be the one used by the test item.
|
||||||
|
write_fixture(fixturedefs[-1])
|
||||||
|
|
||||||
|
for session_item in session.items:
|
||||||
|
write_item(session_item)
|
||||||
|
|
||||||
|
|
||||||
|
def showfixtures(config: Config) -> Union[int, ExitCode]:
|
||||||
|
from _pytest.main import wrap_session
|
||||||
|
|
||||||
|
return wrap_session(config, _showfixtures_main)
|
||||||
|
|
||||||
|
|
||||||
|
def _showfixtures_main(config: Config, session: "Session") -> None:
|
||||||
|
import _pytest.config
|
||||||
|
|
||||||
|
session.perform_collect()
|
||||||
|
invocation_dir = config.invocation_params.dir
|
||||||
|
tw = _pytest.config.create_terminal_writer(config)
|
||||||
|
verbose = config.getvalue("verbose")
|
||||||
|
|
||||||
|
fm = session._fixturemanager
|
||||||
|
|
||||||
|
available = []
|
||||||
|
seen: Set[Tuple[str, str]] = set()
|
||||||
|
|
||||||
|
for argname, fixturedefs in fm._arg2fixturedefs.items():
|
||||||
|
assert fixturedefs is not None
|
||||||
|
if not fixturedefs:
|
||||||
|
continue
|
||||||
|
for fixturedef in fixturedefs:
|
||||||
|
loc = getlocation(fixturedef.func, invocation_dir)
|
||||||
|
if (fixturedef.argname, loc) in seen:
|
||||||
|
continue
|
||||||
|
seen.add((fixturedef.argname, loc))
|
||||||
|
available.append(
|
||||||
|
(
|
||||||
|
len(fixturedef.baseid),
|
||||||
|
fixturedef.func.__module__,
|
||||||
|
_pretty_fixture_path(invocation_dir, fixturedef.func),
|
||||||
|
fixturedef.argname,
|
||||||
|
fixturedef,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
available.sort()
|
||||||
|
currentmodule = None
|
||||||
|
for baseid, module, prettypath, argname, fixturedef in available:
|
||||||
|
if currentmodule != module:
|
||||||
|
if not module.startswith("_pytest."):
|
||||||
|
tw.line()
|
||||||
|
tw.sep("-", f"fixtures defined from {module}")
|
||||||
|
currentmodule = module
|
||||||
|
if verbose <= 0 and argname.startswith("_"):
|
||||||
|
continue
|
||||||
|
tw.write(f"{argname}", green=True)
|
||||||
|
if fixturedef.scope != "function":
|
||||||
|
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
|
||||||
|
tw.write(f" -- {prettypath}", yellow=True)
|
||||||
|
tw.write("\n")
|
||||||
|
doc = inspect.getdoc(fixturedef.func)
|
||||||
|
if doc:
|
||||||
|
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
||||||
|
else:
|
||||||
|
tw.line(" no docstring available", red=True)
|
||||||
|
tw.line()
|
||||||
|
|
||||||
|
|
||||||
|
def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||||
|
for line in doc.split("\n"):
|
||||||
|
tw.line(indent + line)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from errno import ENOENT
|
||||||
from errno import ENOTDIR
|
from errno import ENOTDIR
|
||||||
import fnmatch
|
import fnmatch
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from importlib.machinery import ModuleSpec
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
|
@ -628,11 +629,13 @@ def _import_module_using_spec(
|
||||||
# such as our own assertion-rewrite hook.
|
# such as our own assertion-rewrite hook.
|
||||||
for meta_importer in sys.meta_path:
|
for meta_importer in sys.meta_path:
|
||||||
spec = meta_importer.find_spec(module_name, [str(module_location)])
|
spec = meta_importer.find_spec(module_name, [str(module_location)])
|
||||||
if spec is not None:
|
if spec_matches_module_path(spec, module_path):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
||||||
if spec is not None:
|
|
||||||
|
if spec_matches_module_path(spec, module_path):
|
||||||
|
assert spec is not None
|
||||||
mod = importlib.util.module_from_spec(spec)
|
mod = importlib.util.module_from_spec(spec)
|
||||||
sys.modules[module_name] = mod
|
sys.modules[module_name] = mod
|
||||||
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
||||||
|
@ -643,6 +646,16 @@ def _import_module_using_spec(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def spec_matches_module_path(
|
||||||
|
module_spec: Optional[ModuleSpec], module_path: Path
|
||||||
|
) -> bool:
|
||||||
|
"""Return true if the given ModuleSpec can be used to import the given module path."""
|
||||||
|
if module_spec is None or module_spec.origin is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return Path(module_spec.origin) == module_path
|
||||||
|
|
||||||
|
|
||||||
# Implement a special _is_same function on Windows which returns True if the two filenames
|
# Implement a special _is_same function on Windows which returns True if the two filenames
|
||||||
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
|
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
|
@ -762,39 +775,79 @@ def resolve_pkg_root_and_module_name(
|
||||||
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
|
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
|
||||||
|
|
||||||
If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
|
If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
|
||||||
until we find a directory that is reachable from sys.path, which marks it as a namespace package:
|
for namespace packages:
|
||||||
|
|
||||||
https://packaging.python.org/en/latest/guides/packaging-namespace-packages
|
https://packaging.python.org/en/latest/guides/packaging-namespace-packages
|
||||||
|
|
||||||
Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
|
Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
|
||||||
"""
|
"""
|
||||||
|
pkg_root: Optional[Path] = None
|
||||||
pkg_path = resolve_package_path(path)
|
pkg_path = resolve_package_path(path)
|
||||||
if pkg_path is not None:
|
if pkg_path is not None:
|
||||||
pkg_root = pkg_path.parent
|
pkg_root = pkg_path.parent
|
||||||
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
|
|
||||||
if consider_namespace_packages:
|
if consider_namespace_packages:
|
||||||
# Go upwards in the hierarchy, if we find a parent path included
|
start = pkg_root if pkg_root is not None else path.parent
|
||||||
# in sys.path, it means the package found by resolve_package_path()
|
for candidate in (start, *start.parents):
|
||||||
# actually belongs to a namespace package.
|
module_name = compute_module_name(candidate, path)
|
||||||
for parent in pkg_root.parents:
|
if module_name and is_importable(module_name, path):
|
||||||
# If any of the parent paths has a __init__.py, it means it is not
|
|
||||||
# a namespace package (see the docs linked above).
|
|
||||||
if (parent / "__init__.py").is_file():
|
|
||||||
break
|
|
||||||
if str(parent) in sys.path:
|
|
||||||
# Point the pkg_root to the root of the namespace package.
|
# Point the pkg_root to the root of the namespace package.
|
||||||
pkg_root = parent
|
pkg_root = candidate
|
||||||
break
|
break
|
||||||
|
|
||||||
names = list(path.with_suffix("").relative_to(pkg_root).parts)
|
if pkg_root is not None:
|
||||||
if names[-1] == "__init__":
|
module_name = compute_module_name(pkg_root, path)
|
||||||
names.pop()
|
if module_name:
|
||||||
module_name = ".".join(names)
|
|
||||||
return pkg_root, module_name
|
return pkg_root, module_name
|
||||||
|
|
||||||
raise CouldNotResolvePathError(f"Could not resolve for {path}")
|
raise CouldNotResolvePathError(f"Could not resolve for {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_importable(module_name: str, module_path: Path) -> bool:
|
||||||
|
"""
|
||||||
|
Return if the given module path could be imported normally by Python, akin to the user
|
||||||
|
entering the REPL and importing the corresponding module name directly, and corresponds
|
||||||
|
to the module_path specified.
|
||||||
|
|
||||||
|
:param module_name:
|
||||||
|
Full module name that we want to check if is importable.
|
||||||
|
For example, "app.models".
|
||||||
|
|
||||||
|
:param module_path:
|
||||||
|
Full path to the python module/package we want to check if is importable.
|
||||||
|
For example, "/projects/src/app/models.py".
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through
|
||||||
|
# sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``).
|
||||||
|
# Using importlib.util.find_spec() is different, it gives the same results as trying to import
|
||||||
|
# the module normally in the REPL.
|
||||||
|
spec = importlib.util.find_spec(module_name)
|
||||||
|
except (ImportError, ValueError, ImportWarning):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return spec_matches_module_path(spec, module_path)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_module_name(root: Path, module_path: Path) -> Optional[str]:
|
||||||
|
"""Compute a module name based on a path and a root anchor."""
|
||||||
|
try:
|
||||||
|
path_without_suffix = module_path.with_suffix("")
|
||||||
|
except ValueError:
|
||||||
|
# Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter).
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
relative = path_without_suffix.relative_to(root)
|
||||||
|
except ValueError: # pragma: no cover
|
||||||
|
return None
|
||||||
|
names = list(relative.parts)
|
||||||
|
if not names:
|
||||||
|
return None
|
||||||
|
if names[-1] == "__init__":
|
||||||
|
names.pop()
|
||||||
|
return ".".join(names)
|
||||||
|
|
||||||
|
|
||||||
class CouldNotResolvePathError(Exception):
|
class CouldNotResolvePathError(Exception):
|
||||||
"""Custom exception raised by resolve_pkg_root_and_module_name."""
|
"""Custom exception raised by resolve_pkg_root_and_module_name."""
|
||||||
|
|
||||||
|
|
|
@ -40,13 +40,11 @@ from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest._code.code import Traceback
|
from _pytest._code.code import Traceback
|
||||||
from _pytest._io import TerminalWriter
|
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import ascii_escaped
|
from _pytest.compat import ascii_escaped
|
||||||
from _pytest.compat import get_default_arg_names
|
from _pytest.compat import get_default_arg_names
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import getimfunc
|
from _pytest.compat import getimfunc
|
||||||
from _pytest.compat import getlocation
|
|
||||||
from _pytest.compat import is_async_function
|
from _pytest.compat import is_async_function
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
|
@ -54,7 +52,6 @@ from _pytest.compat import NOTSET
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
from _pytest.compat import safe_isclass
|
from _pytest.compat import safe_isclass
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
|
@ -71,7 +68,6 @@ from _pytest.mark.structures import MarkDecorator
|
||||||
from _pytest.mark.structures import normalize_mark_list
|
from _pytest.mark.structures import normalize_mark_list
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
from _pytest.pathlib import bestrelpath
|
|
||||||
from _pytest.pathlib import fnmatch_ex
|
from _pytest.pathlib import fnmatch_ex
|
||||||
from _pytest.pathlib import import_path
|
from _pytest.pathlib import import_path
|
||||||
from _pytest.pathlib import ImportPathMismatchError
|
from _pytest.pathlib import ImportPathMismatchError
|
||||||
|
@ -88,27 +84,7 @@ if TYPE_CHECKING:
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
|
|
||||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
group = parser.getgroup("general")
|
|
||||||
group.addoption(
|
|
||||||
"--fixtures",
|
|
||||||
"--funcargs",
|
|
||||||
action="store_true",
|
|
||||||
dest="showfixtures",
|
|
||||||
default=False,
|
|
||||||
help="Show available fixtures, sorted by plugin appearance "
|
|
||||||
"(fixtures with leading '_' are only shown with '-v')",
|
|
||||||
)
|
|
||||||
group.addoption(
|
|
||||||
"--fixtures-per-test",
|
|
||||||
action="store_true",
|
|
||||||
dest="show_fixtures_per_test",
|
|
||||||
default=False,
|
|
||||||
help="Show fixtures per test",
|
|
||||||
)
|
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"python_files",
|
"python_files",
|
||||||
type="args",
|
type="args",
|
||||||
|
@ -137,16 +113,6 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
|
||||||
if config.option.showfixtures:
|
|
||||||
showfixtures(config)
|
|
||||||
return 0
|
|
||||||
if config.option.show_fixtures_per_test:
|
|
||||||
show_fixtures_per_test(config)
|
|
||||||
return 0
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
||||||
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
||||||
metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
|
metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
|
||||||
|
@ -1525,137 +1491,6 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
|
||||||
return val if escape_option else ascii_escaped(val) # type: ignore
|
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
|
||||||
loc = Path(getlocation(func, invocation_dir))
|
|
||||||
prefix = Path("...", "_pytest")
|
|
||||||
try:
|
|
||||||
return str(prefix / loc.relative_to(_PYTEST_DIR))
|
|
||||||
except ValueError:
|
|
||||||
return bestrelpath(invocation_dir, loc)
|
|
||||||
|
|
||||||
|
|
||||||
def show_fixtures_per_test(config):
|
|
||||||
from _pytest.main import wrap_session
|
|
||||||
|
|
||||||
return wrap_session(config, _show_fixtures_per_test)
|
|
||||||
|
|
||||||
|
|
||||||
def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
|
||||||
import _pytest.config
|
|
||||||
|
|
||||||
session.perform_collect()
|
|
||||||
invocation_dir = config.invocation_params.dir
|
|
||||||
tw = _pytest.config.create_terminal_writer(config)
|
|
||||||
verbose = config.getvalue("verbose")
|
|
||||||
|
|
||||||
def get_best_relpath(func) -> str:
|
|
||||||
loc = getlocation(func, invocation_dir)
|
|
||||||
return bestrelpath(invocation_dir, Path(loc))
|
|
||||||
|
|
||||||
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
|
|
||||||
argname = fixture_def.argname
|
|
||||||
if verbose <= 0 and argname.startswith("_"):
|
|
||||||
return
|
|
||||||
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
|
|
||||||
tw.write(f"{argname}", green=True)
|
|
||||||
tw.write(f" -- {prettypath}", yellow=True)
|
|
||||||
tw.write("\n")
|
|
||||||
fixture_doc = inspect.getdoc(fixture_def.func)
|
|
||||||
if fixture_doc:
|
|
||||||
write_docstring(
|
|
||||||
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
tw.line(" no docstring available", red=True)
|
|
||||||
|
|
||||||
def write_item(item: nodes.Item) -> None:
|
|
||||||
# Not all items have _fixtureinfo attribute.
|
|
||||||
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
|
||||||
if info is None or not info.name2fixturedefs:
|
|
||||||
# This test item does not use any fixtures.
|
|
||||||
return
|
|
||||||
tw.line()
|
|
||||||
tw.sep("-", f"fixtures used by {item.name}")
|
|
||||||
# TODO: Fix this type ignore.
|
|
||||||
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
|
||||||
# dict key not used in loop but needed for sorting.
|
|
||||||
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
|
||||||
assert fixturedefs is not None
|
|
||||||
if not fixturedefs:
|
|
||||||
continue
|
|
||||||
# Last item is expected to be the one used by the test item.
|
|
||||||
write_fixture(fixturedefs[-1])
|
|
||||||
|
|
||||||
for session_item in session.items:
|
|
||||||
write_item(session_item)
|
|
||||||
|
|
||||||
|
|
||||||
def showfixtures(config: Config) -> Union[int, ExitCode]:
|
|
||||||
from _pytest.main import wrap_session
|
|
||||||
|
|
||||||
return wrap_session(config, _showfixtures_main)
|
|
||||||
|
|
||||||
|
|
||||||
def _showfixtures_main(config: Config, session: Session) -> None:
|
|
||||||
import _pytest.config
|
|
||||||
|
|
||||||
session.perform_collect()
|
|
||||||
invocation_dir = config.invocation_params.dir
|
|
||||||
tw = _pytest.config.create_terminal_writer(config)
|
|
||||||
verbose = config.getvalue("verbose")
|
|
||||||
|
|
||||||
fm = session._fixturemanager
|
|
||||||
|
|
||||||
available = []
|
|
||||||
seen: Set[Tuple[str, str]] = set()
|
|
||||||
|
|
||||||
for argname, fixturedefs in fm._arg2fixturedefs.items():
|
|
||||||
assert fixturedefs is not None
|
|
||||||
if not fixturedefs:
|
|
||||||
continue
|
|
||||||
for fixturedef in fixturedefs:
|
|
||||||
loc = getlocation(fixturedef.func, invocation_dir)
|
|
||||||
if (fixturedef.argname, loc) in seen:
|
|
||||||
continue
|
|
||||||
seen.add((fixturedef.argname, loc))
|
|
||||||
available.append(
|
|
||||||
(
|
|
||||||
len(fixturedef.baseid),
|
|
||||||
fixturedef.func.__module__,
|
|
||||||
_pretty_fixture_path(invocation_dir, fixturedef.func),
|
|
||||||
fixturedef.argname,
|
|
||||||
fixturedef,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
available.sort()
|
|
||||||
currentmodule = None
|
|
||||||
for baseid, module, prettypath, argname, fixturedef in available:
|
|
||||||
if currentmodule != module:
|
|
||||||
if not module.startswith("_pytest."):
|
|
||||||
tw.line()
|
|
||||||
tw.sep("-", f"fixtures defined from {module}")
|
|
||||||
currentmodule = module
|
|
||||||
if verbose <= 0 and argname.startswith("_"):
|
|
||||||
continue
|
|
||||||
tw.write(f"{argname}", green=True)
|
|
||||||
if fixturedef.scope != "function":
|
|
||||||
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
|
|
||||||
tw.write(f" -- {prettypath}", yellow=True)
|
|
||||||
tw.write("\n")
|
|
||||||
doc = inspect.getdoc(fixturedef.func)
|
|
||||||
if doc:
|
|
||||||
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
|
||||||
else:
|
|
||||||
tw.line(" no docstring available", red=True)
|
|
||||||
tw.line()
|
|
||||||
|
|
||||||
|
|
||||||
def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
|
||||||
for line in doc.split("\n"):
|
|
||||||
tw.line(indent + line)
|
|
||||||
|
|
||||||
|
|
||||||
class Function(PyobjMixin, nodes.Item):
|
class Function(PyobjMixin, nodes.Item):
|
||||||
"""Item responsible for setting up and executing a Python test function.
|
"""Item responsible for setting up and executing a Python test function.
|
||||||
|
|
||||||
|
|
|
@ -1408,11 +1408,11 @@ def _get_line_with_reprcrash_message(
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if not running_on_ci():
|
if running_on_ci() or config.option.verbose >= 2:
|
||||||
|
msg = f" - {msg}"
|
||||||
|
else:
|
||||||
available_width = tw.fullwidth - line_width
|
available_width = tw.fullwidth - line_width
|
||||||
msg = _format_trimmed(" - {}", msg, available_width)
|
msg = _format_trimmed(" - {}", msg, available_width)
|
||||||
else:
|
|
||||||
msg = f" - {msg}"
|
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
line += msg
|
line += msg
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
anyio[curio,trio]==4.3.0
|
anyio[curio,trio]==4.3.0
|
||||||
django==5.0.3
|
django==5.0.4
|
||||||
pytest-asyncio==0.23.6
|
pytest-asyncio==0.23.6
|
||||||
# Temporarily not installed until pytest-bdd is fixed:
|
# Temporarily not installed until pytest-bdd is fixed:
|
||||||
# https://github.com/pytest-dev/pytest/pull/11785
|
# https://github.com/pytest-dev/pytest/pull/11785
|
||||||
# pytest-bdd==7.0.1
|
# pytest-bdd==7.0.1
|
||||||
pytest-cov==4.1.0
|
pytest-cov==5.0.0
|
||||||
pytest-django==4.8.0
|
pytest-django==4.8.0
|
||||||
pytest-flakes==4.0.5
|
pytest-flakes==4.0.5
|
||||||
pytest-html==4.1.1
|
pytest-html==4.1.1
|
||||||
|
|
|
@ -1731,8 +1731,8 @@ class TestEarlyRewriteBailout:
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as newpath:
|
||||||
os.chdir(d)
|
os.chdir(newpath)
|
||||||
""",
|
""",
|
||||||
"test_test.py": """\
|
"test_test.py": """\
|
||||||
def test():
|
def test():
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
# mypy: allow-untyped-defs
|
from enum import auto
|
||||||
|
from enum import Enum
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from _pytest.compat import assert_never
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
@ -175,7 +180,9 @@ class TestNewAPI:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir")))
|
@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir")))
|
||||||
def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
def test_cache_reportheader(
|
||||||
|
env: Sequence[str], pytester: Pytester, monkeypatch: MonkeyPatch
|
||||||
|
) -> None:
|
||||||
pytester.makepyfile("""def test_foo(): pass""")
|
pytester.makepyfile("""def test_foo(): pass""")
|
||||||
if env:
|
if env:
|
||||||
monkeypatch.setenv(*env)
|
monkeypatch.setenv(*env)
|
||||||
|
@ -507,7 +514,7 @@ class TestLastFailed:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def rlf(fail_import, fail_run):
|
def rlf(fail_import: int, fail_run: int) -> Any:
|
||||||
monkeypatch.setenv("FAILIMPORT", str(fail_import))
|
monkeypatch.setenv("FAILIMPORT", str(fail_import))
|
||||||
monkeypatch.setenv("FAILTEST", str(fail_run))
|
monkeypatch.setenv("FAILTEST", str(fail_run))
|
||||||
|
|
||||||
|
@ -555,7 +562,9 @@ class TestLastFailed:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def rlf(fail_import, fail_run, args=()):
|
def rlf(
|
||||||
|
fail_import: int, fail_run: int, args: Sequence[str] = ()
|
||||||
|
) -> Tuple[Any, Any]:
|
||||||
monkeypatch.setenv("FAILIMPORT", str(fail_import))
|
monkeypatch.setenv("FAILIMPORT", str(fail_import))
|
||||||
monkeypatch.setenv("FAILTEST", str(fail_run))
|
monkeypatch.setenv("FAILTEST", str(fail_run))
|
||||||
|
|
||||||
|
@ -1254,20 +1263,41 @@ class TestReadme:
|
||||||
assert self.check_readme(pytester) is True
|
assert self.check_readme(pytester) is True
|
||||||
|
|
||||||
|
|
||||||
def test_gitignore(pytester: Pytester) -> None:
|
class Action(Enum):
|
||||||
|
"""Action to perform on the cache directory."""
|
||||||
|
|
||||||
|
MKDIR = auto()
|
||||||
|
SET = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("action", list(Action))
|
||||||
|
def test_gitignore(
|
||||||
|
pytester: Pytester,
|
||||||
|
action: Action,
|
||||||
|
) -> None:
|
||||||
"""Ensure we automatically create .gitignore file in the pytest_cache directory (#3286)."""
|
"""Ensure we automatically create .gitignore file in the pytest_cache directory (#3286)."""
|
||||||
from _pytest.cacheprovider import Cache
|
from _pytest.cacheprovider import Cache
|
||||||
|
|
||||||
config = pytester.parseconfig()
|
config = pytester.parseconfig()
|
||||||
cache = Cache.for_config(config, _ispytest=True)
|
cache = Cache.for_config(config, _ispytest=True)
|
||||||
|
if action == Action.MKDIR:
|
||||||
|
cache.mkdir("foo")
|
||||||
|
elif action == Action.SET:
|
||||||
cache.set("foo", "bar")
|
cache.set("foo", "bar")
|
||||||
|
else:
|
||||||
|
assert_never(action)
|
||||||
msg = "# Created by pytest automatically.\n*\n"
|
msg = "# Created by pytest automatically.\n*\n"
|
||||||
gitignore_path = cache._cachedir.joinpath(".gitignore")
|
gitignore_path = cache._cachedir.joinpath(".gitignore")
|
||||||
assert gitignore_path.read_text(encoding="UTF-8") == msg
|
assert gitignore_path.read_text(encoding="UTF-8") == msg
|
||||||
|
|
||||||
# Does not overwrite existing/custom one.
|
# Does not overwrite existing/custom one.
|
||||||
gitignore_path.write_text("custom", encoding="utf-8")
|
gitignore_path.write_text("custom", encoding="utf-8")
|
||||||
|
if action == Action.MKDIR:
|
||||||
|
cache.mkdir("something")
|
||||||
|
elif action == Action.SET:
|
||||||
cache.set("something", "else")
|
cache.set("something", "else")
|
||||||
|
else:
|
||||||
|
assert_never(action)
|
||||||
assert gitignore_path.read_text(encoding="UTF-8") == "custom"
|
assert gitignore_path.read_text(encoding="UTF-8") == "custom"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
import errno
|
import errno
|
||||||
|
import importlib.abc
|
||||||
|
import importlib.machinery
|
||||||
import os.path
|
import os.path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pickle
|
import pickle
|
||||||
|
@ -10,12 +12,15 @@ from types import ModuleType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
|
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
from _pytest.pathlib import commonpath
|
from _pytest.pathlib import commonpath
|
||||||
|
from _pytest.pathlib import compute_module_name
|
||||||
from _pytest.pathlib import CouldNotResolvePathError
|
from _pytest.pathlib import CouldNotResolvePathError
|
||||||
from _pytest.pathlib import ensure_deletable
|
from _pytest.pathlib import ensure_deletable
|
||||||
from _pytest.pathlib import fnmatch_ex
|
from _pytest.pathlib import fnmatch_ex
|
||||||
|
@ -25,6 +30,7 @@ from _pytest.pathlib import import_path
|
||||||
from _pytest.pathlib import ImportMode
|
from _pytest.pathlib import ImportMode
|
||||||
from _pytest.pathlib import ImportPathMismatchError
|
from _pytest.pathlib import ImportPathMismatchError
|
||||||
from _pytest.pathlib import insert_missing_modules
|
from _pytest.pathlib import insert_missing_modules
|
||||||
|
from _pytest.pathlib import is_importable
|
||||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||||
from _pytest.pathlib import module_name_from_path
|
from _pytest.pathlib import module_name_from_path
|
||||||
from _pytest.pathlib import resolve_package_path
|
from _pytest.pathlib import resolve_package_path
|
||||||
|
@ -33,6 +39,7 @@ from _pytest.pathlib import safe_exists
|
||||||
from _pytest.pathlib import symlink_or_skip
|
from _pytest.pathlib import symlink_or_skip
|
||||||
from _pytest.pathlib import visit
|
from _pytest.pathlib import visit
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
from _pytest.pytester import RunResult
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -717,12 +724,13 @@ class TestImportLibMode:
|
||||||
assert result == "_env_310.tests.test_foo"
|
assert result == "_env_310.tests.test_foo"
|
||||||
|
|
||||||
def test_resolve_pkg_root_and_module_name(
|
def test_resolve_pkg_root_and_module_name(
|
||||||
self, tmp_path: Path, monkeypatch: MonkeyPatch
|
self, tmp_path: Path, monkeypatch: MonkeyPatch, pytester: Pytester
|
||||||
) -> None:
|
) -> None:
|
||||||
# Create a directory structure first without __init__.py files.
|
# Create a directory structure first without __init__.py files.
|
||||||
(tmp_path / "src/app/core").mkdir(parents=True)
|
(tmp_path / "src/app/core").mkdir(parents=True)
|
||||||
models_py = tmp_path / "src/app/core/models.py"
|
models_py = tmp_path / "src/app/core/models.py"
|
||||||
models_py.touch()
|
models_py.touch()
|
||||||
|
|
||||||
with pytest.raises(CouldNotResolvePathError):
|
with pytest.raises(CouldNotResolvePathError):
|
||||||
_ = resolve_pkg_root_and_module_name(models_py)
|
_ = resolve_pkg_root_and_module_name(models_py)
|
||||||
|
|
||||||
|
@ -738,6 +746,8 @@ class TestImportLibMode:
|
||||||
|
|
||||||
# If we add tmp_path to sys.path, src becomes a namespace package.
|
# If we add tmp_path to sys.path, src becomes a namespace package.
|
||||||
monkeypatch.syspath_prepend(tmp_path)
|
monkeypatch.syspath_prepend(tmp_path)
|
||||||
|
validate_namespace_package(pytester, [tmp_path], ["src.app.core.models"])
|
||||||
|
|
||||||
assert resolve_pkg_root_and_module_name(
|
assert resolve_pkg_root_and_module_name(
|
||||||
models_py, consider_namespace_packages=True
|
models_py, consider_namespace_packages=True
|
||||||
) == (
|
) == (
|
||||||
|
@ -1119,37 +1129,54 @@ def test_safe_exists(tmp_path: Path) -> None:
|
||||||
class TestNamespacePackages:
|
class TestNamespacePackages:
|
||||||
"""Test import_path support when importing from properly namespace packages."""
|
"""Test import_path support when importing from properly namespace packages."""
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup_imports_tracking(self, monkeypatch: MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(sys, "pytest_namespace_packages_test", [], raising=False)
|
||||||
|
|
||||||
def setup_directories(
|
def setup_directories(
|
||||||
self, tmp_path: Path, monkeypatch: MonkeyPatch, pytester: Pytester
|
self, tmp_path: Path, monkeypatch: Optional[MonkeyPatch], pytester: Pytester
|
||||||
) -> Tuple[Path, Path]:
|
) -> Tuple[Path, Path]:
|
||||||
|
# Use a code to guard against modules being imported more than once.
|
||||||
|
# This is a safeguard in case future changes break this invariant.
|
||||||
|
code = dedent(
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
imported = getattr(sys, "pytest_namespace_packages_test", [])
|
||||||
|
assert __name__ not in imported, f"{__name__} already imported"
|
||||||
|
imported.append(__name__)
|
||||||
|
sys.pytest_namespace_packages_test = imported
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Set up a namespace package "com.company", containing
|
# Set up a namespace package "com.company", containing
|
||||||
# two subpackages, "app" and "calc".
|
# two subpackages, "app" and "calc".
|
||||||
(tmp_path / "src/dist1/com/company/app/core").mkdir(parents=True)
|
(tmp_path / "src/dist1/com/company/app/core").mkdir(parents=True)
|
||||||
(tmp_path / "src/dist1/com/company/app/__init__.py").touch()
|
(tmp_path / "src/dist1/com/company/app/__init__.py").write_text(
|
||||||
(tmp_path / "src/dist1/com/company/app/core/__init__.py").touch()
|
code, encoding="UTF-8"
|
||||||
|
)
|
||||||
|
(tmp_path / "src/dist1/com/company/app/core/__init__.py").write_text(
|
||||||
|
code, encoding="UTF-8"
|
||||||
|
)
|
||||||
models_py = tmp_path / "src/dist1/com/company/app/core/models.py"
|
models_py = tmp_path / "src/dist1/com/company/app/core/models.py"
|
||||||
models_py.touch()
|
models_py.touch()
|
||||||
|
|
||||||
(tmp_path / "src/dist2/com/company/calc/algo").mkdir(parents=True)
|
(tmp_path / "src/dist2/com/company/calc/algo").mkdir(parents=True)
|
||||||
(tmp_path / "src/dist2/com/company/calc/__init__.py").touch()
|
(tmp_path / "src/dist2/com/company/calc/__init__.py").write_text(
|
||||||
(tmp_path / "src/dist2/com/company/calc/algo/__init__.py").touch()
|
code, encoding="UTF-8"
|
||||||
algorithms_py = tmp_path / "src/dist2/com/company/calc/algo/algorithms.py"
|
|
||||||
algorithms_py.touch()
|
|
||||||
|
|
||||||
# Validate the namespace package by importing it in a Python subprocess.
|
|
||||||
r = pytester.runpython_c(
|
|
||||||
dedent(
|
|
||||||
f"""
|
|
||||||
import sys
|
|
||||||
sys.path.append(r{str(tmp_path / "src/dist1")!r})
|
|
||||||
sys.path.append(r{str(tmp_path / "src/dist2")!r})
|
|
||||||
import com.company.app.core.models
|
|
||||||
import com.company.calc.algo.algorithms
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
|
(tmp_path / "src/dist2/com/company/calc/algo/__init__.py").write_text(
|
||||||
|
code, encoding="UTF-8"
|
||||||
|
)
|
||||||
|
algorithms_py = tmp_path / "src/dist2/com/company/calc/algo/algorithms.py"
|
||||||
|
algorithms_py.write_text(code, encoding="UTF-8")
|
||||||
|
|
||||||
|
r = validate_namespace_package(
|
||||||
|
pytester,
|
||||||
|
[tmp_path / "src/dist1", tmp_path / "src/dist2"],
|
||||||
|
["com.company.app.core.models", "com.company.calc.algo.algorithms"],
|
||||||
)
|
)
|
||||||
assert r.ret == 0
|
assert r.ret == 0
|
||||||
|
if monkeypatch is not None:
|
||||||
monkeypatch.syspath_prepend(tmp_path / "src/dist1")
|
monkeypatch.syspath_prepend(tmp_path / "src/dist1")
|
||||||
monkeypatch.syspath_prepend(tmp_path / "src/dist2")
|
monkeypatch.syspath_prepend(tmp_path / "src/dist2")
|
||||||
return models_py, algorithms_py
|
return models_py, algorithms_py
|
||||||
|
@ -1223,11 +1250,76 @@ class TestNamespacePackages:
|
||||||
models_py, algorithms_py = self.setup_directories(
|
models_py, algorithms_py = self.setup_directories(
|
||||||
tmp_path, monkeypatch, pytester
|
tmp_path, monkeypatch, pytester
|
||||||
)
|
)
|
||||||
# Namespace packages must not have an __init__.py at any of its
|
# Namespace packages must not have an __init__.py at its top-level
|
||||||
# directories; if it does, we then fall back to importing just the
|
# directory; if it does, it is no longer a namespace package, and we fall back
|
||||||
# part of the package containing the __init__.py files.
|
# to importing just the part of the package containing the __init__.py files.
|
||||||
(tmp_path / "src/dist1/com/__init__.py").touch()
|
(tmp_path / "src/dist1/com/__init__.py").touch()
|
||||||
|
|
||||||
|
# Because of the __init__ file, 'com' is no longer a namespace package:
|
||||||
|
# 'com.company.app' is importable as a normal module.
|
||||||
|
# 'com.company.calc' is no longer importable because 'com' is not a namespace package anymore.
|
||||||
|
r = validate_namespace_package(
|
||||||
|
pytester,
|
||||||
|
[tmp_path / "src/dist1", tmp_path / "src/dist2"],
|
||||||
|
["com.company.app.core.models", "com.company.calc.algo.algorithms"],
|
||||||
|
)
|
||||||
|
assert r.ret == 1
|
||||||
|
r.stderr.fnmatch_lines("*No module named 'com.company.calc*")
|
||||||
|
|
||||||
|
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||||
|
models_py, consider_namespace_packages=True
|
||||||
|
)
|
||||||
|
assert (pkg_root, module_name) == (
|
||||||
|
tmp_path / "src/dist1",
|
||||||
|
"com.company.app.core.models",
|
||||||
|
)
|
||||||
|
|
||||||
|
# dist2/com/company will contain a normal Python package.
|
||||||
|
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||||
|
algorithms_py, consider_namespace_packages=True
|
||||||
|
)
|
||||||
|
assert (pkg_root, module_name) == (
|
||||||
|
tmp_path / "src/dist2/com/company",
|
||||||
|
"calc.algo.algorithms",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_detect_meta_path(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
pytester: Pytester,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
resolve_pkg_root_and_module_name() considers sys.meta_path when importing namespace packages.
|
||||||
|
|
||||||
|
Regression test for #12112.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomImporter(importlib.abc.MetaPathFinder):
|
||||||
|
"""
|
||||||
|
Imports the module name "com" as a namespace package.
|
||||||
|
|
||||||
|
This ensures our namespace detection considers sys.meta_path, which is important
|
||||||
|
to support all possible ways a module can be imported (for example editable installs).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def find_spec(
|
||||||
|
self, name: str, path: Any = None, target: Any = None
|
||||||
|
) -> Optional[importlib.machinery.ModuleSpec]:
|
||||||
|
if name == "com":
|
||||||
|
spec = importlib.machinery.ModuleSpec("com", loader=None)
|
||||||
|
spec.submodule_search_locations = [str(com_root_2), str(com_root_1)]
|
||||||
|
return spec
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Setup directories without configuring sys.path.
|
||||||
|
models_py, algorithms_py = self.setup_directories(
|
||||||
|
tmp_path, monkeypatch=None, pytester=pytester
|
||||||
|
)
|
||||||
|
com_root_1 = tmp_path / "src/dist1/com"
|
||||||
|
com_root_2 = tmp_path / "src/dist2/com"
|
||||||
|
|
||||||
|
# Because the namespace package is not setup correctly, we cannot resolve it as a namespace package.
|
||||||
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||||
models_py, consider_namespace_packages=True
|
models_py, consider_namespace_packages=True
|
||||||
)
|
)
|
||||||
|
@ -1235,3 +1327,107 @@ class TestNamespacePackages:
|
||||||
tmp_path / "src/dist1/com/company",
|
tmp_path / "src/dist1/com/company",
|
||||||
"app.core.models",
|
"app.core.models",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Insert our custom importer, which will recognize the "com" directory as a namespace package.
|
||||||
|
new_meta_path = [CustomImporter(), *sys.meta_path]
|
||||||
|
monkeypatch.setattr(sys, "meta_path", new_meta_path)
|
||||||
|
|
||||||
|
# Now we should be able to resolve the path as namespace package.
|
||||||
|
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||||
|
models_py, consider_namespace_packages=True
|
||||||
|
)
|
||||||
|
assert (pkg_root, module_name) == (
|
||||||
|
tmp_path / "src/dist1",
|
||||||
|
"com.company.app.core.models",
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("insert", [True, False])
|
||||||
|
def test_full_ns_packages_without_init_files(
|
||||||
|
self, pytester: Pytester, tmp_path: Path, monkeypatch: MonkeyPatch, insert: bool
|
||||||
|
) -> None:
|
||||||
|
(tmp_path / "src/dist1/ns/b/app/bar/test").mkdir(parents=True)
|
||||||
|
(tmp_path / "src/dist1/ns/b/app/bar/m.py").touch()
|
||||||
|
|
||||||
|
if insert:
|
||||||
|
# The presence of this __init__.py is not a problem, ns.b.app is still part of the namespace package.
|
||||||
|
(tmp_path / "src/dist1/ns/b/app/__init__.py").touch()
|
||||||
|
|
||||||
|
(tmp_path / "src/dist2/ns/a/core/foo/test").mkdir(parents=True)
|
||||||
|
(tmp_path / "src/dist2/ns/a/core/foo/m.py").touch()
|
||||||
|
|
||||||
|
# Validate the namespace package by importing it in a Python subprocess.
|
||||||
|
r = validate_namespace_package(
|
||||||
|
pytester,
|
||||||
|
[tmp_path / "src/dist1", tmp_path / "src/dist2"],
|
||||||
|
["ns.b.app.bar.m", "ns.a.core.foo.m"],
|
||||||
|
)
|
||||||
|
assert r.ret == 0
|
||||||
|
monkeypatch.syspath_prepend(tmp_path / "src/dist1")
|
||||||
|
monkeypatch.syspath_prepend(tmp_path / "src/dist2")
|
||||||
|
|
||||||
|
assert resolve_pkg_root_and_module_name(
|
||||||
|
tmp_path / "src/dist1/ns/b/app/bar/m.py", consider_namespace_packages=True
|
||||||
|
) == (tmp_path / "src/dist1", "ns.b.app.bar.m")
|
||||||
|
assert resolve_pkg_root_and_module_name(
|
||||||
|
tmp_path / "src/dist2/ns/a/core/foo/m.py", consider_namespace_packages=True
|
||||||
|
) == (tmp_path / "src/dist2", "ns.a.core.foo.m")
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_importable(pytester: Pytester) -> None:
|
||||||
|
pytester.syspathinsert()
|
||||||
|
|
||||||
|
path = pytester.path / "bar/foo.py"
|
||||||
|
path.parent.mkdir()
|
||||||
|
path.touch()
|
||||||
|
assert is_importable("bar.foo", path) is True
|
||||||
|
|
||||||
|
# Ensure that the module that can be imported points to the path we expect.
|
||||||
|
path = pytester.path / "some/other/path/bar/foo.py"
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
assert is_importable("bar.foo", path) is False
|
||||||
|
|
||||||
|
# Paths containing "." cannot be imported.
|
||||||
|
path = pytester.path / "bar.x/__init__.py"
|
||||||
|
path.parent.mkdir()
|
||||||
|
path.touch()
|
||||||
|
assert is_importable("bar.x", path) is False
|
||||||
|
|
||||||
|
# Pass starting with "." denote relative imports and cannot be checked using is_importable.
|
||||||
|
path = pytester.path / ".bar.x/__init__.py"
|
||||||
|
path.parent.mkdir()
|
||||||
|
path.touch()
|
||||||
|
assert is_importable(".bar.x", path) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_module_name(tmp_path: Path) -> None:
|
||||||
|
assert compute_module_name(tmp_path, tmp_path) is None
|
||||||
|
assert compute_module_name(Path(), Path()) is None
|
||||||
|
|
||||||
|
assert compute_module_name(tmp_path, tmp_path / "mod.py") == "mod"
|
||||||
|
assert compute_module_name(tmp_path, tmp_path / "src/app/bar") == "src.app.bar"
|
||||||
|
assert compute_module_name(tmp_path, tmp_path / "src/app/bar.py") == "src.app.bar"
|
||||||
|
assert (
|
||||||
|
compute_module_name(tmp_path, tmp_path / "src/app/bar/__init__.py")
|
||||||
|
== "src.app.bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_namespace_package(
|
||||||
|
pytester: Pytester, paths: Sequence[Path], modules: Sequence[str]
|
||||||
|
) -> RunResult:
|
||||||
|
"""
|
||||||
|
Validate that a Python namespace package is set up correctly.
|
||||||
|
|
||||||
|
In a sub interpreter, add 'paths' to sys.path and attempt to import the given modules.
|
||||||
|
|
||||||
|
In this module many tests configure a set of files as a namespace package, this function
|
||||||
|
is used as sanity check that our files are configured correctly from the point of view of Python.
|
||||||
|
"""
|
||||||
|
lines = [
|
||||||
|
"import sys",
|
||||||
|
# Configure sys.path.
|
||||||
|
*[f"sys.path.append(r{str(x)!r})" for x in paths],
|
||||||
|
# Imports.
|
||||||
|
*[f"import {x}" for x in modules],
|
||||||
|
]
|
||||||
|
return pytester.runpython_c("\n".join(lines))
|
||||||
|
|
|
@ -2377,8 +2377,13 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None:
|
||||||
|
|
||||||
monkeypatch.setattr(_pytest.terminal, "_get_node_id_with_markup", mock_get_pos)
|
monkeypatch.setattr(_pytest.terminal, "_get_node_id_with_markup", mock_get_pos)
|
||||||
|
|
||||||
|
class Namespace:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
class config:
|
class config:
|
||||||
pass
|
def __init__(self):
|
||||||
|
self.option = Namespace(verbose=0)
|
||||||
|
|
||||||
class rep:
|
class rep:
|
||||||
def _get_verbose_word(self, *args):
|
def _get_verbose_word(self, *args):
|
||||||
|
@ -2399,7 +2404,7 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None:
|
||||||
if msg:
|
if msg:
|
||||||
rep.longrepr.reprcrash.message = msg # type: ignore
|
rep.longrepr.reprcrash.message = msg # type: ignore
|
||||||
actual = _get_line_with_reprcrash_message(
|
actual = _get_line_with_reprcrash_message(
|
||||||
config, # type: ignore[arg-type]
|
config(), # type: ignore[arg-type]
|
||||||
rep(), # type: ignore[arg-type]
|
rep(), # type: ignore[arg-type]
|
||||||
DummyTerminalWriter(), # type: ignore[arg-type]
|
DummyTerminalWriter(), # type: ignore[arg-type]
|
||||||
{},
|
{},
|
||||||
|
@ -2443,6 +2448,43 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None:
|
||||||
check("🉐🉐🉐🉐🉐\n2nd line", 80, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐🉐🉐")
|
check("🉐🉐🉐🉐🉐\n2nd line", 80, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐🉐🉐")
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_summary_with_verbose(
|
||||||
|
monkeypatch: MonkeyPatch, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
"""With -vv do not truncate the summary info (#11777)."""
|
||||||
|
# On CI we also do not truncate the summary info, monkeypatch it to ensure we
|
||||||
|
# are testing against the -vv flag on CI.
|
||||||
|
monkeypatch.setattr(_pytest.terminal, "running_on_ci", lambda: False)
|
||||||
|
|
||||||
|
string_length = 200
|
||||||
|
pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
def test():
|
||||||
|
s1 = "A" * {string_length}
|
||||||
|
s2 = "B" * {string_length}
|
||||||
|
assert s1 == s2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# No -vv, summary info should be truncated.
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*short test summary info*",
|
||||||
|
"* assert 'AAA...",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# No truncation with -vv.
|
||||||
|
result = pytester.runpytest("-vv")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*short test summary info*",
|
||||||
|
f"*{'A' * string_length}*{'B' * string_length}'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"seconds, expected",
|
"seconds, expected",
|
||||||
[
|
[
|
||||||
|
|
Loading…
Reference in New Issue