Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pytest-dev/pytest
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 7.4.1
Choose a base ref
...
head repository: pytest-dev/pytest
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 7.4.2
Choose a head ref
  • 11 commits
  • 21 files changed
  • 8 contributors

Commits on Sep 2, 2023

  1. Checkout source code during deploy

    We need the checked out repository in order to push the tag.
    nicoddemus committed Sep 2, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    0319a0d View commit details
  2. Merge pull request #11377 from pytest-dev/release-7.4.1

    Prepare release 7.4.1
    nicoddemus authored Sep 2, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    82eb86f View commit details

Commits on Sep 3, 2023

  1. Fix user_properties not saved to XML if fixture errors during teardown (

    #11382)
    
    Move handling of user_properties to `finalize()`.
    
    Previously if a fixture failed during teardown, `pytest_runtest_logreport` would not be called with "teardown", resulting in the user properties not being saved on the JUnit XML file.
    
    Fixes: #11367
    (cherry picked from commit 917ce9a)
    
    Co-authored-by: Israel Fruchter <israel.fruchter@gmail.com>
    nicoddemus and fruch authored Sep 3, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    7f5d9b9 View commit details

Commits on Sep 5, 2023

  1. [7.4.x] Fix import_path for packages (#11395)

    Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
    github-actions[bot] and nicoddemus authored Sep 5, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    1de00e9 View commit details

Commits on Sep 6, 2023

  1. [7.4.x] improve plugin list disclaimer (#11398)

    Co-authored-by: Stefaan Lippens <soxofaan@users.noreply.github.com>
    github-actions[bot] and soxofaan authored Sep 6, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    de69883 View commit details
  2. [7.4.x] doc: Remove done training (#11400)

    Co-authored-by: Florian Bruhin <me@the-compiler.org>
    github-actions[bot] and The-Compiler authored Sep 6, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    79c2012 View commit details

Commits on Sep 7, 2023

  1. [7.4.x] Fix doctest collection of functools.cached_property objects. (

    #11403)
    
    Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
    github-actions[bot] and RonnyPfannschmidt authored Sep 7, 2023
    Copy the full SHA
    6e49a74 View commit details
  2. Fix crash when passing a very long cmdline argument (#11404)

    Fixes #11394
    
    (cherry picked from commit 28ccf47)
    nicoddemus committed Sep 7, 2023
    Copy the full SHA
    884b911 View commit details
  3. Use _pytest.pathlib.safe_exists in get_dirs_from_args

    Related to #11394
    nicoddemus committed Sep 7, 2023
    Copy the full SHA
    63b0c6f View commit details
  4. Merge pull request #11406 from nicoddemus/backport-11404-to-7.4.x

    [7.4.x] Fix crash when passing a very long cmdline argument (#11404)
    nicoddemus authored Sep 7, 2023
    Copy the full SHA
    e4f022f View commit details
  5. Prepare release version 7.4.2

    pytestbot committed Sep 7, 2023
    Copy the full SHA
    45f34df View commit details
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/checkout@v3
- name: Download Package
uses: actions/download-artifact@v3
with:
4 changes: 4 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -166,6 +166,8 @@ Ian Bicking
Ian Lesperance
Ilya Konstantinov
Ionuț Turturică
Isaac Virshup
Israel Fruchter
Itxaso Aizpurua
Iwan Briquemont
Jaap Broekhuizen
@@ -339,6 +341,7 @@ Simon Holesch
Simon Kerr
Skylar Downes
Srinivas Reddy Thatiparthy
Stefaan Lippens
Stefan Farmbauer
Stefan Scherfke
Stefan Zimmermann
@@ -371,6 +374,7 @@ Tony Narlock
Tor Colvin
Trevor Bekolay
Tyler Goodlet
Tyler Smart
Tzu-ping Chung
Vasily Kuznetsov
Victor Maryama
1 change: 1 addition & 0 deletions doc/en/announce/index.rst
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2


release-7.4.2
release-7.4.1
release-7.4.0
release-7.3.2
18 changes: 18 additions & 0 deletions doc/en/announce/release-7.4.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
pytest-7.4.2
=======================================

pytest 7.4.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:

* Bruno Oliveira


Happy testing,
The pytest Development Team
2 changes: 1 addition & 1 deletion doc/en/builtin.rst
Original file line number Diff line number Diff line change
@@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsys.readouterr()
assert captured.out == "hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
25 changes: 25 additions & 0 deletions doc/en/changelog.rst
Original file line number Diff line number Diff line change
@@ -28,6 +28,31 @@ with advance notice in the **Deprecations** section of releases.

.. towncrier release notes start
pytest 7.4.2 (2023-09-07)
=========================

Bug Fixes
---------

- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.


- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.


- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.


- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.



Improved Documentation
----------------------

- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.


pytest 7.4.1 (2023-09-02)
=========================

20 changes: 10 additions & 10 deletions doc/en/example/reportingdemo.rst
Original file line number Diff line number Diff line change
@@ -554,13 +554,13 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E AssertionError: assert False
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0006>()
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
failure_demo.py:235: AssertionError
_____________________ TestMoreErrors.test_global_func ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
def test_global_func(self):
> assert isinstance(globf(42), float)
@@ -571,18 +571,18 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:238: AssertionError
_______________________ TestMoreErrors.test_instance _______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
def test_instance(self):
self.x = 6 * 7
> assert self.x != 42
E assert 42 != 42
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>.x
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x
failure_demo.py:242: AssertionError
_______________________ TestMoreErrors.test_compare ________________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
def test_compare(self):
> assert globf(10) < 5
@@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:245: AssertionError
_____________________ TestMoreErrors.test_try_finally ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
def test_try_finally(self):
x = 1
@@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:250: AssertionError
___________________ TestCustomAssertMsg.test_single_line ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
def test_single_line(self):
class A:
@@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:261: AssertionError
____________________ TestCustomAssertMsg.test_multiline ____________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
def test_multiline(self):
class A:
@@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:268: AssertionError
___________________ TestCustomAssertMsg.test_custom_repr ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
def test_custom_repr(self):
class JSON:
2 changes: 1 addition & 1 deletion doc/en/getting-started.rst
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 7.4.1
pytest 7.4.2
.. _`simpletest`:

1 change: 0 additions & 1 deletion doc/en/index.rst
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@

.. sidebar:: Next Open Trainings

- `pytest: Professionelles Testen (nicht nur) für Python <https://workshoptage.ch/workshops/2023/pytest-professionelles-testen-nicht-nur-fuer-python-2/>`_, at `Workshoptage 2023 <https://workshoptage.ch/>`_, **September 5th**, `OST <https://www.ost.ch/en>`_ Campus **Rapperswil, Switzerland**
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**

Also see :doc:`previous talks and blogposts <talks>`.
22 changes: 17 additions & 5 deletions doc/en/reference/plugin_list.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@

.. _plugin-list:

Plugin List
===========
Pytest Plugin List
==================

PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
Packages classified as inactive are excluded.

For detailed insights into how this list is generated,
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.

.. warning::

Please be aware that this list is not a curated collection of projects
and does not undergo a systematic review process.
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.

Do not presume any endorsement from the ``pytest`` project or its developers,
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.


.. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the
page.
22 changes: 17 additions & 5 deletions scripts/update-plugin-list.py
Original file line number Diff line number Diff line change
@@ -13,14 +13,26 @@
FILE_HEAD = r"""
.. _plugin-list:
Plugin List
===========
Pytest Plugin List
==================
PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
Packages classified as inactive are excluded.
For detailed insights into how this list is generated,
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
.. warning::
Please be aware that this list is not a curated collection of projects
and does not undergo a systematic review process.
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
Do not presume any endorsement from the ``pytest`` project or its developers,
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
.. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the
page.
9 changes: 3 additions & 6 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import safe_exists
from _pytest.stash import Stash
from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
@@ -557,12 +558,8 @@ def _set_initial_conftests(
anchor = absolutepath(current / path)

# 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:
# is in fact a very long option (#10169, #11394).
if safe_exists(anchor):
self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True
if not foundanchor:
9 changes: 1 addition & 8 deletions src/_pytest/config/findpaths.py
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath
from _pytest.pathlib import safe_exists

if TYPE_CHECKING:
from . import Config
@@ -151,14 +152,6 @@ def get_dir_from_path(path: Path) -> Path:
return path
return path.parent

def safe_exists(path: Path) -> bool:
# This can throw on paths that contain characters unrepresentable at the OS level,
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
try:
return path.exists()
except OSError:
return False

# These look like paths but may not exist
possible_paths = (
absolutepath(get_file_part_from_node_id(arg))
20 changes: 20 additions & 0 deletions src/_pytest/doctest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Discover and run doctests in modules and test files."""
import bdb
import functools
import inspect
import os
import platform
@@ -536,6 +537,25 @@ def _find(
tests, obj, name, module, source_lines, globs, seen
)

if sys.version_info < (3, 13):

def _from_module(self, module, object):
"""`cached_property` objects are never considered a part
of the 'current module'. As such they are skipped by doctest.
Here we override `_from_module` to check the underlying
function instead. https://github.com/python/cpython/issues/107995
"""
if hasattr(functools, "cached_property") and isinstance(
object, functools.cached_property
):
object = object.func

# Type ignored because this is a private function.
return super()._from_module(module, object) # type: ignore[misc]

else: # pragma: no cover
pass

if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest(
self.path,
7 changes: 4 additions & 3 deletions src/_pytest/junitxml.py
Original file line number Diff line number Diff line change
@@ -502,6 +502,10 @@ def finalize(self, report: TestReport) -> None:
# Local hack to handle xdist report order.
workernode = getattr(report, "node", None)
reporter = self.node_reporters.pop((nodeid, workernode))

for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))

if reporter is not None:
reporter.finalize()

@@ -599,9 +603,6 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
reporter = self._opentestcase(report)
reporter.write_captured_output(report)

for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))

self.finalize(report)
report_wid = getattr(report, "worker_id", None)
report_ii = getattr(report, "item_index", None)
3 changes: 2 additions & 1 deletion src/_pytest/main.py
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import safe_exists
from _pytest.pathlib import visit
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
@@ -895,7 +896,7 @@ def resolve_collection_argument(
strpath = search_pypath(strpath)
fspath = invocation_path / strpath
fspath = absolutepath(fspath)
if not fspath.exists():
if not safe_exists(fspath):
msg = (
"module or package not found: {arg} (missing __init__.py?)"
if as_pypath
14 changes: 14 additions & 0 deletions src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
@@ -623,6 +623,10 @@ def module_name_from_path(path: Path, root: Path) -> str:
# Use the parts for the relative path to the root path.
path_parts = relative_path.parts

# Module name for packages do not contain the __init__ file.
if path_parts[-1] == "__init__":
path_parts = path_parts[:-1]

return ".".join(path_parts)


@@ -787,3 +791,13 @@ def copytree(source: Path, target: Path) -> None:
shutil.copyfile(x, newx)
elif x.is_dir():
newx.mkdir(exist_ok=True)


def safe_exists(p: Path) -> bool:
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
try:
return p.exists()
except (ValueError, OSError):
# ValueError: stat: path too long for Windows
# OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
return False
21 changes: 21 additions & 0 deletions testing/test_doctest.py
Original file line number Diff line number Diff line change
@@ -482,6 +482,27 @@ def test_doctestmodule(self, pytester: Pytester):
reprec = pytester.inline_run(p, "--doctest-modules")
reprec.assertoutcome(failed=1)

@pytest.mark.skipif(
sys.version_info[:2] <= (3, 7), reason="Only Python 3.7 or less"
)
def test_doctest_cached_property(self, pytester: Pytester):
p = pytester.makepyfile(
"""
import functools
class Foo:
@functools.cached_property
def foo(self):
'''
>>> assert False, "Tacos!"
'''
...
"""
)
result = pytester.runpytest(p, "--doctest-modules")
result.assert_outcomes(failed=1)
assert "Tacos!" in result.stdout.str()

def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
p = pytester.mkpydir("hello")
p.joinpath("__init__.py").write_text(
30 changes: 30 additions & 0 deletions testing/test_junitxml.py
Original file line number Diff line number Diff line change
@@ -1228,6 +1228,36 @@ def test_record(record_property, other):
result.stdout.fnmatch_lines(["*= 1 passed in *"])


def test_record_property_on_test_and_teardown_failure(
pytester: Pytester, run_and_parse: RunAndParse
) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.fixture
def other(record_property):
record_property("bar", 1)
yield
assert 0
def test_record(record_property, other):
record_property("foo", "<1")
assert 0
"""
)
result, dom = run_and_parse()
node = dom.find_first_by_tag("testsuite")
tnodes = node.find_by_tag("testcase")
for tnode in tnodes:
psnode = tnode.find_first_by_tag("properties")
assert psnode, f"testcase didn't had expected properties:\n{tnode}"
pnodes = psnode.find_by_tag("property")
pnodes[0].assert_attr(name="bar", value="1")
pnodes[1].assert_attr(name="foo", value="<1")
result.stdout.fnmatch_lines(["*= 1 failed, 1 error *"])


def test_record_property_same_name(
pytester: Pytester, run_and_parse: RunAndParse
) -> None:
31 changes: 31 additions & 0 deletions testing/test_main.py
Original file line number Diff line number Diff line change
@@ -262,3 +262,34 @@ def test(fix):
"* 1 passed in *",
]
)


def test_very_long_cmdline_arg(pytester: Pytester) -> None:
"""
Regression test for #11394.
Note: we could not manage to actually reproduce the error with this code, we suspect
GitHub runners are configured to support very long paths, however decided to leave
the test in place in case this ever regresses in the future.
"""
pytester.makeconftest(
"""
import pytest
def pytest_addoption(parser):
parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things")
@pytest.fixture(scope="module")
def specified_feeds(request):
list_string = request.config.getoption("--long-list")
return list_string.split(',')
"""
)
pytester.makepyfile(
"""
def test_foo(specified_feeds):
assert len(specified_feeds) == 100_000
"""
)
result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000))
result.stdout.fnmatch_lines("* 1 passed *")
76 changes: 76 additions & 0 deletions testing/test_pathlib.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import errno
import os.path
import pickle
import sys
@@ -18,11 +19,13 @@
from _pytest.pathlib import get_extended_length_path_str
from _pytest.pathlib import get_lock_path
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import insert_missing_modules
from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import module_name_from_path
from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import safe_exists
from _pytest.pathlib import symlink_or_skip
from _pytest.pathlib import visit
from _pytest.tmpdir import TempPathFactory
@@ -585,6 +588,10 @@ def test_module_name_from_path(self, tmp_path: Path) -> None:
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
assert result == "home.foo.test_foo"

# Importing __init__.py files should return the package as module name.
result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
assert result == "src.app"

def test_insert_missing_modules(
self, monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
@@ -615,3 +622,72 @@ def test_parent_contains_child_module_attribute(
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
assert modules["xxx"].tests is modules["xxx.tests"]
assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]

def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path):
"""
Importing a package using --importmode=importlib should not import the
package's __init__.py file more than once (#11306).
"""
monkeypatch.chdir(tmp_path)
monkeypatch.syspath_prepend(tmp_path)

package_name = "importlib_import_package"
tmp_path.joinpath(package_name).mkdir()
init = tmp_path.joinpath(f"{package_name}/__init__.py")
init.write_text(
dedent(
"""
from .singleton import Singleton
instance = Singleton()
"""
),
encoding="ascii",
)
singleton = tmp_path.joinpath(f"{package_name}/singleton.py")
singleton.write_text(
dedent(
"""
class Singleton:
INSTANCES = []
def __init__(self) -> None:
self.INSTANCES.append(self)
if len(self.INSTANCES) > 1:
raise RuntimeError("Already initialized")
"""
),
encoding="ascii",
)

mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
assert len(mod.instance.INSTANCES) == 1


def test_safe_exists(tmp_path: Path) -> None:
d = tmp_path.joinpath("some_dir")
d.mkdir()
assert safe_exists(d) is True

f = tmp_path.joinpath("some_file")
f.touch()
assert safe_exists(f) is True

# Use unittest.mock() as a context manager to have a very narrow
# patch lifetime.
p = tmp_path.joinpath("some long filename" * 100)
with unittest.mock.patch.object(
Path,
"exists",
autospec=True,
side_effect=OSError(errno.ENAMETOOLONG, "name too long"),
):
assert safe_exists(p) is False

with unittest.mock.patch.object(
Path,
"exists",
autospec=True,
side_effect=ValueError("name too long"),
):
assert safe_exists(p) is False