Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/13807.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:meth:`monkeypatch.syspath_prepend() <pytest.MonkeyPatch.syspath_prepend>` now issues a deprecation warning when the prepended path contains legacy namespace packages (those using ``pkg_resources.declare_namespace()``).
Users should migrate to native namespace packages (:pep:`420`).
See :ref:`monkeypatch-fixup-namespace-packages` for details.
35 changes: 35 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,41 @@ Below is a complete list of all pytest features which are considered deprecated.
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.


.. _monkeypatch-fixup-namespace-packages:

``monkeypatch.syspath_prepend`` with legacy namespace packages
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: 9.0

When using :meth:`monkeypatch.syspath_prepend() <pytest.MonkeyPatch.syspath_prepend>`,
pytest automatically calls ``pkg_resources.fixup_namespace_packages()`` if ``pkg_resources`` is imported.
This is only needed for legacy namespace packages that use ``pkg_resources.declare_namespace()``.

Legacy namespace packages are deprecated in favor of native namespace packages (:pep:`420`).
If you are using ``pkg_resources.declare_namespace()`` in your ``__init__.py`` files,
you should migrate to native namespace packages by removing the ``__init__.py`` files from your namespace packages.

This deprecation warning will only be issued when:

1. ``pkg_resources`` is imported, and
2. The specific path being prepended contains a declared namespace package (via ``pkg_resources.declare_namespace()``)

To fix this warning, convert your legacy namespace packages to native namespace packages:

**Legacy namespace package** (deprecated):

.. code-block:: python

# mypkg/__init__.py
__import__("pkg_resources").declare_namespace(__name__)

**Native namespace package** (recommended):

Simply remove the ``__init__.py`` file entirely.
Python 3.3+ natively supports namespace packages without ``__init__.py``.


.. _sync-test-async-fixture:

sync test depending on async fixture
Expand Down
8 changes: 8 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import PytestRemovedIn10Warning
from _pytest.warning_types import UnformattedWarning


Expand Down Expand Up @@ -66,6 +67,13 @@
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
)

MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning(
"monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n"
"Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n"
"Please use native namespace packages (PEP 420) instead.\n"
"See https://docs.pytest.org/en/stable/deprecations.html#monkeypatch-fixup-namespace-packages"
)

# You want to make some `__init__` or function "private".
#
# def my_private_function(some, args):
Expand Down
20 changes: 20 additions & 0 deletions src/_pytest/monkeypatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections.abc import MutableMapping
from contextlib import contextmanager
import os
from pathlib import Path
import re
import sys
from typing import Any
Expand All @@ -16,6 +17,7 @@
from typing import TypeVar
import warnings

from _pytest.deprecated import MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES
from _pytest.fixtures import fixture
from _pytest.warning_types import PytestWarning

Expand Down Expand Up @@ -346,8 +348,26 @@ def syspath_prepend(self, path) -> None:
# https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
# this is only needed when pkg_resources was already loaded by the namespace package
if "pkg_resources" in sys.modules:
import pkg_resources
from pkg_resources import fixup_namespace_packages

# Only issue deprecation warning if this call would actually have an
# effect for this specific path.
if (
hasattr(pkg_resources, "_namespace_packages")
and pkg_resources._namespace_packages
):
path_obj = Path(str(path))
for ns_pkg in pkg_resources._namespace_packages:
if ns_pkg is None:
continue
ns_pkg_path = path_obj / ns_pkg.replace(".", os.sep)
if ns_pkg_path.is_dir():
warnings.warn(
MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES, stacklevel=2
)
break

fixup_namespace_packages(str(path))

# A call to syspathinsert() usually means that the caller wants to
Expand Down
25 changes: 17 additions & 8 deletions testing/test_monkeypatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
import re
import sys
import textwrap

import setuptools
import warnings

from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
Expand Down Expand Up @@ -430,14 +429,16 @@ class A:
assert A.x == 1


@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning")
@pytest.mark.skipif(
int(setuptools.__version__.split(".")[0]) >= 80,
reason="modern setuptools removing pkg_resources",
@pytest.mark.filterwarnings(
r"ignore:.*\bpkg_resources\b:DeprecationWarning",
r"ignore:.*\bpkg_resources\b:UserWarning",
)
def test_syspath_prepend_with_namespace_packages(
pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
# Needs to be in sys.modules.
pytest.importorskip("pkg_resources")

for dirname in "hello", "world":
d = pytester.mkdir(dirname)
ns = d.joinpath("ns_pkg")
Expand All @@ -451,7 +452,9 @@ def test_syspath_prepend_with_namespace_packages(
f"def check(): return {dirname!r}", encoding="utf-8"
)

# First call should not warn - namespace package not registered yet.
monkeypatch.syspath_prepend("hello")
# This registers ns_pkg as a namespace package.
import ns_pkg.hello

assert ns_pkg.hello.check() == "hello"
Expand All @@ -460,13 +463,19 @@ def test_syspath_prepend_with_namespace_packages(
import ns_pkg.world

# Prepending should call fixup_namespace_packages.
monkeypatch.syspath_prepend("world")
# This call should warn - ns_pkg is now registered and "world" contains it
with pytest.warns(pytest.PytestRemovedIn10Warning, match="legacy namespace"):
monkeypatch.syspath_prepend("world")
import ns_pkg.world

assert ns_pkg.world.check() == "world"

# Should invalidate caches via importlib.invalidate_caches.
# Should not warn for path without namespace packages.
modules_tmpdir = pytester.mkdir("modules_tmpdir")
monkeypatch.syspath_prepend(str(modules_tmpdir))
with warnings.catch_warnings():
warnings.simplefilter("error")
monkeypatch.syspath_prepend(str(modules_tmpdir))

modules_tmpdir.joinpath("main_app.py").write_text("app = True", encoding="utf-8")
from main_app import app # noqa: F401