Skip to content

Commit 8fb067f

Browse files
committed
monkeypatch: deprecate call to fixup_namespace_packages in syspath_prepend
Fix #13807.
1 parent 37acc4f commit 8fb067f

File tree

5 files changed

+83
-8
lines changed

5 files changed

+83
-8
lines changed

changelog/13807.deprecation.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
: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()``).
2+
Users should migrate to native namespace packages (:pep:`420`).
3+
See :ref:`monkeypatch-fixup-namespace-packages` for details.

doc/en/deprecations.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,41 @@ Below is a complete list of all pytest features which are considered deprecated.
1515
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
1616

1717

18+
.. _monkeypatch-fixup-namespace-packages:
19+
20+
``monkeypatch.syspath_prepend`` with legacy namespace packages
21+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22+
23+
.. deprecated:: 9.0
24+
25+
When using :meth:`monkeypatch.syspath_prepend() <pytest.MonkeyPatch.syspath_prepend>`,
26+
pytest automatically calls ``pkg_resources.fixup_namespace_packages()`` if ``pkg_resources`` is imported.
27+
This is only needed for legacy namespace packages that use ``pkg_resources.declare_namespace()``.
28+
29+
Legacy namespace packages are deprecated in favor of native namespace packages (:pep:`420`).
30+
If you are using ``pkg_resources.declare_namespace()`` in your ``__init__.py`` files,
31+
you should migrate to native namespace packages by removing the ``__init__.py`` files from your namespace packages.
32+
33+
This deprecation warning will only be issued when:
34+
35+
1. ``pkg_resources`` is imported, and
36+
2. The specific path being prepended contains a declared namespace package (via ``pkg_resources.declare_namespace()``)
37+
38+
To fix this warning, convert your legacy namespace packages to native namespace packages:
39+
40+
**Legacy namespace package** (deprecated):
41+
42+
.. code-block:: python
43+
44+
# mypkg/__init__.py
45+
__import__("pkg_resources").declare_namespace(__name__)
46+
47+
**Native namespace package** (recommended):
48+
49+
Simply remove the ``__init__.py`` file entirely.
50+
Python 3.3+ natively supports namespace packages without ``__init__.py``.
51+
52+
1853
.. _sync-test-async-fixture:
1954

2055
sync test depending on async fixture

src/_pytest/deprecated.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from _pytest.warning_types import PytestDeprecationWarning
1717
from _pytest.warning_types import PytestRemovedIn9Warning
18+
from _pytest.warning_types import PytestRemovedIn10Warning
1819
from _pytest.warning_types import UnformattedWarning
1920

2021

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

70+
MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning(
71+
"monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n"
72+
"Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n"
73+
"Please use native namespace packages (PEP 420) instead.\n"
74+
"See https://docs.pytest.org/en/stable/deprecations.html#monkeypatch-fixup-namespace-packages"
75+
)
76+
6977
# You want to make some `__init__` or function "private".
7078
#
7179
# def my_private_function(some, args):

src/_pytest/monkeypatch.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections.abc import MutableMapping
99
from contextlib import contextmanager
1010
import os
11+
from pathlib import Path
1112
import re
1213
import sys
1314
from typing import Any
@@ -16,6 +17,7 @@
1617
from typing import TypeVar
1718
import warnings
1819

20+
from _pytest.deprecated import MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES
1921
from _pytest.fixtures import fixture
2022
from _pytest.warning_types import PytestWarning
2123

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

354+
# Only issue deprecation warning if this call would actually have an
355+
# effect for this specific path.
356+
if (
357+
hasattr(pkg_resources, "_namespace_packages")
358+
and pkg_resources._namespace_packages
359+
):
360+
path_obj = Path(str(path))
361+
for ns_pkg in pkg_resources._namespace_packages:
362+
if ns_pkg is None:
363+
continue
364+
ns_pkg_path = path_obj / ns_pkg.replace(".", os.sep)
365+
if ns_pkg_path.is_dir():
366+
warnings.warn(
367+
MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES, stacklevel=2
368+
)
369+
break
370+
351371
fixup_namespace_packages(str(path))
352372

353373
# A call to syspathinsert() usually means that the caller wants to

testing/test_monkeypatch.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
import re
88
import sys
99
import textwrap
10-
11-
import setuptools
10+
import warnings
1211

1312
from _pytest.monkeypatch import MonkeyPatch
1413
from _pytest.pytester import Pytester
@@ -430,14 +429,16 @@ class A:
430429
assert A.x == 1
431430

432431

433-
@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning")
434-
@pytest.mark.skipif(
435-
int(setuptools.__version__.split(".")[0]) >= 80,
436-
reason="modern setuptools removing pkg_resources",
432+
@pytest.mark.filterwarnings(
433+
r"ignore:.*\bpkg_resources\b:DeprecationWarning",
434+
r"ignore:.*\bpkg_resources\b:UserWarning",
437435
)
438436
def test_syspath_prepend_with_namespace_packages(
439437
pytester: Pytester, monkeypatch: MonkeyPatch
440438
) -> None:
439+
# Needs to be in sys.modules.
440+
pytest.importorskip("pkg_resources")
441+
441442
for dirname in "hello", "world":
442443
d = pytester.mkdir(dirname)
443444
ns = d.joinpath("ns_pkg")
@@ -451,7 +452,9 @@ def test_syspath_prepend_with_namespace_packages(
451452
f"def check(): return {dirname!r}", encoding="utf-8"
452453
)
453454

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

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

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

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

468473
# Should invalidate caches via importlib.invalidate_caches.
474+
# Should not warn for path without namespace packages.
469475
modules_tmpdir = pytester.mkdir("modules_tmpdir")
470-
monkeypatch.syspath_prepend(str(modules_tmpdir))
476+
with warnings.catch_warnings():
477+
warnings.simplefilter("error")
478+
monkeypatch.syspath_prepend(str(modules_tmpdir))
479+
471480
modules_tmpdir.joinpath("main_app.py").write_text("app = True", encoding="utf-8")
472481
from main_app import app # noqa: F401

0 commit comments

Comments
 (0)