Skip to content

Commit

Permalink
Merge branch 'pytest-dev:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
shekhuverma committed Apr 26, 2024
2 parents 76fd0e5 + fafab1d commit a55abeb
Show file tree
Hide file tree
Showing 18 changed files with 402 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/update-plugin-list.yml
Expand Up @@ -46,7 +46,7 @@ jobs:
run: python scripts/update-plugin-list.py

- name: Create Pull Request
uses: peter-evans/create-pull-request@c55203cfde3e5c11a452d352b4393e68b85b4533
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>'
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.3.7"
rev: "v0.4.1"
hooks:
- id: ruff
args: ["--fix"]
Expand Down Expand Up @@ -34,7 +34,7 @@ repos:
additional_dependencies:
- iniconfig>=1.1.0
- attrs>=19.2.0
- pluggy>=1.4.0
- pluggy>=1.5.0
- packaging
- tomli
- types-pkg_resources
Expand All @@ -43,7 +43,7 @@ repos:
# on <3.11
- exceptiongroup>=1.0.0rc8
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "1.7.0"
rev: "1.8.0"
hooks:
- id: pyproject-fmt
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
Expand Down
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -321,6 +321,7 @@ Pierre Sassoulas
Pieter Mulder
Piotr Banaszkiewicz
Piotr Helm
Poulami Sau
Prakhar Gurunani
Prashant Anand
Prashant Sharma
Expand Down
12 changes: 12 additions & 0 deletions changelog/12069.deprecation.rst
@@ -0,0 +1,12 @@
A deprecation warning is now raised when implementations of one of the following hooks request a deprecated ``py.path.local`` parameter instead of the ``pathlib.Path`` parameter which replaced it:

- :hook:`pytest_ignore_collect` - the ``path`` parameter - use ``collection_path`` instead.
- :hook:`pytest_collect_file` - the ``path`` parameter - use ``file_path`` instead.
- :hook:`pytest_pycollect_makemodule` - the ``path`` parameter - use ``module_path`` instead.
- :hook:`pytest_report_header` - the ``startdir`` parameter - use ``start_path`` instead.
- :hook:`pytest_report_collectionfinish` - the ``startdir`` parameter - use ``start_path`` instead.

The replacement parameters are available since pytest 7.0.0.
The old parameters will be removed in pytest 9.0.0.

See :ref:`legacy-path-hooks-deprecated` for more details.
1 change: 1 addition & 0 deletions changelog/12069.trivial.rst
@@ -0,0 +1 @@
``pluggy>=1.5.0`` is now required.
1 change: 1 addition & 0 deletions changelog/12114.bugfix.rst
@@ -0,0 +1 @@
Fixed attribute error in pytest.approx for types implicitly convertible to numpy arrays by converting other_side to a numpy array so that np_array_shape != other_side.shape can be properly checked.
1 change: 1 addition & 0 deletions changelog/12194.bugfix.rst
@@ -0,0 +1 @@
Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules.
3 changes: 2 additions & 1 deletion doc/en/index.rst
Expand Up @@ -5,7 +5,8 @@
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
* **June 11th to 13th 2024**, Remote
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, **June 17th -- 22nd 2024**
- pytest tips and tricks for a better testsuite, `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague

Also see :doc:`previous talks and blogposts <talks>`.

Expand Down
130 changes: 85 additions & 45 deletions doc/en/reference/plugin_list.rst

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion doc/en/requirements.txt
@@ -1,5 +1,5 @@
pallets-sphinx-themes
pluggy>=1.2.0
pluggy>=1.5.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
sphinx>=7
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Expand Up @@ -43,7 +43,7 @@ dependencies = [
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
"iniconfig",
"packaging",
"pluggy<2.0,>=1.4",
"pluggy<2.0,>=1.5",
'tomli>=1; python_version < "3.11"',
]
[project.optional-dependencies]
Expand Down
41 changes: 39 additions & 2 deletions src/_pytest/hookspec.py
Expand Up @@ -15,6 +15,8 @@

from pluggy import HookspecMarker

from .deprecated import HOOK_LEGACY_PATH_ARG


if TYPE_CHECKING:
import pdb
Expand Down Expand Up @@ -297,7 +299,14 @@ def pytest_collection_finish(session: "Session") -> None:
"""


@hookspec(firstresult=True)
@hookspec(
firstresult=True,
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="collection_path"
),
},
)
def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
Expand Down Expand Up @@ -356,6 +365,13 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
"""


@hookspec(
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="file_path"
),
},
)
def pytest_collect_file(
file_path: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]":
Expand Down Expand Up @@ -468,7 +484,14 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
# -------------------------------------------------------------------------


@hookspec(firstresult=True)
@hookspec(
firstresult=True,
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="module_path"
),
},
)
def pytest_pycollect_makemodule(
module_path: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]:
Expand Down Expand Up @@ -994,6 +1017,13 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
# -------------------------------------------------------------------------


@hookspec(
warn_on_impl_args={
"startdir": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="startdir", pathlib_path_arg="start_path"
),
},
)
def pytest_report_header( # type:ignore[empty-body]
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]:
Expand Down Expand Up @@ -1022,6 +1052,13 @@ def pytest_report_header( # type:ignore[empty-body]
"""


@hookspec(
warn_on_impl_args={
"startdir": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="startdir", pathlib_path_arg="start_path"
),
},
)
def pytest_report_collectionfinish( # type:ignore[empty-body]
config: "Config",
start_path: Path,
Expand Down
105 changes: 68 additions & 37 deletions src/_pytest/pathlib.py
@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import atexit
import contextlib
from enum import Enum
Expand All @@ -23,6 +22,7 @@
import sys
import types
from types import ModuleType
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
Expand Down Expand Up @@ -59,7 +59,7 @@
)


def _ignore_error(exception):
def _ignore_error(exception: Exception) -> bool:
return (
getattr(exception, "errno", None) in _IGNORED_ERRORS
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
Expand All @@ -71,7 +71,7 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:


def on_rm_rf_error(
func,
func: Optional[Callable[..., Any]],
path: str,
excinfo: Union[
BaseException,
Expand Down Expand Up @@ -196,7 +196,7 @@ def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
return extract_suffixes(find_prefixed(root, prefix), prefix)


def parse_num(maybe_num) -> int:
def parse_num(maybe_num: str) -> int:
"""Parse number path suffixes, returns -1 on error."""
try:
return int(maybe_num)
Expand Down Expand Up @@ -264,7 +264,9 @@ def create_cleanup_lock(p: Path) -> Path:
return lock_path


def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
def register_cleanup_lock_removal(
lock_path: Path, register: Any = atexit.register
) -> Any:
"""Register a cleanup function for removing a lock, by default on atexit."""
pid = os.getpid()

Expand Down Expand Up @@ -355,7 +357,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
yield Path(entry)


def cleanup_dead_symlinks(root: Path):
def cleanup_dead_symlinks(root: Path) -> None:
for left_dir in root.iterdir():
if left_dir.is_symlink():
if not left_dir.resolve().exists():
Expand Down Expand Up @@ -459,10 +461,14 @@ def parts(s: str) -> Set[str]:
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}


def symlink_or_skip(src, dst, **kwargs):
def symlink_or_skip(
src: Union["os.PathLike[str]", str],
dst: Union["os.PathLike[str]", str],
**kwargs: Any,
) -> None:
"""Make a symlink, or skip the test in case symlinks are not supported."""
try:
os.symlink(str(src), str(dst), **kwargs)
os.symlink(src, dst, **kwargs)
except OSError as e:
skip(f"symlinks not supported: {e}")

Expand Down Expand Up @@ -620,10 +626,6 @@ def _import_module_using_spec(
:param insert_modules:
If True, will call insert_missing_modules to create empty intermediate modules
for made-up module names (when importing test files not reachable from sys.path).
Note: we can probably drop insert_missing_modules altogether: instead of
generating module names such as "src.tests.test_foo", which require intermediate
empty modules, we might just as well generate unique module names like
"src_tests_test_foo".
"""
# Checking with sys.meta_path first in case one of its hooks can import this module,
# such as our own assertion-rewrite hook.
Expand All @@ -636,9 +638,41 @@ def _import_module_using_spec(

if spec_matches_module_path(spec, module_path):
assert spec is not None
# Attempt to import the parent module, seems is our responsibility:
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
parent_module_name, _, name = module_name.rpartition(".")
parent_module: Optional[ModuleType] = None
if parent_module_name:
parent_module = sys.modules.get(parent_module_name)
if parent_module is None:
# Find the directory of this module's parent.
parent_dir = (
module_path.parent.parent
if module_path.name == "__init__.py"
else module_path.parent
)
# Consider the parent module path as its __init__.py file, if it has one.
parent_module_path = (
parent_dir / "__init__.py"
if (parent_dir / "__init__.py").is_file()
else parent_dir
)
parent_module = _import_module_using_spec(
parent_module_name,
parent_module_path,
parent_dir,
insert_modules=insert_modules,
)

# Find spec and import this module.
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod) # type: ignore[union-attr]

# Set this module as an attribute of the parent module (#12194).
if parent_module is not None:
setattr(parent_module, name, mod)

if insert_modules:
insert_missing_modules(sys.modules, module_name)
return mod
Expand Down Expand Up @@ -709,34 +743,31 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
otherwise "src.tests.test_foo" is not importable by ``__import__``.
"""
module_parts = module_name.split(".")
child_module: Union[ModuleType, None] = None
module: Union[ModuleType, None] = None
child_name: str = ""
while module_name:
if module_name not in modules:
try:
# If sys.meta_path is empty, calling import_module will issue
# a warning and raise ModuleNotFoundError. To avoid the
# warning, we check sys.meta_path explicitly and raise the error
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
module = importlib.import_module(module_name)
except ModuleNotFoundError:
module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
else:
module = modules[module_name]
if child_module:
parent_module_name, _, child_name = module_name.rpartition(".")
if parent_module_name:
parent_module = modules.get(parent_module_name)
if parent_module is None:
try:
# If sys.meta_path is empty, calling import_module will issue
# a warning and raise ModuleNotFoundError. To avoid the
# warning, we check sys.meta_path explicitly and raise the error
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
parent_module = importlib.import_module(parent_module_name)
except ModuleNotFoundError:
parent_module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
modules[parent_module_name] = parent_module

# Add child attribute to the parent that can reference the child
# modules.
if not hasattr(module, child_name):
setattr(module, child_name, child_module)
modules[module_name] = module
# Keep track of the child module while moving up the tree.
child_module, child_name = module, module_name.rpartition(".")[-1]
if not hasattr(parent_module, child_name):
setattr(parent_module, child_name, modules[module_name])

module_parts.pop(-1)
module_name = ".".join(module_parts)

Expand Down

0 comments on commit a55abeb

Please sign in to comment.