Skip to content

Commit f5fab2b

Browse files
authored
Merge master into features (#5698)
Merge master into features
2 parents f7e925d + aa06e6c commit f5fab2b

File tree

8 files changed

+106
-29
lines changed

8 files changed

+106
-29
lines changed

changelog/4344.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only.

changelog/5684.trivial.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace manual handling of ``OSError.errno`` in the codebase by new ``OSError`` subclasses (``PermissionError``, ``FileNotFoundError``, etc.).

doc/en/usage.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,19 @@ Running ``pytest`` can result in six different exit codes:
3333
:Exit code 4: pytest command line usage error
3434
:Exit code 5: No tests were collected
3535

36-
They are represented by the :class:`_pytest.main.ExitCode` enum.
36+
They are represented by the :class:`_pytest.main.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
37+
38+
.. code-block:: python
39+
40+
from pytest import ExitCode
41+
42+
.. note::
43+
44+
If you would like to customize the exit code in some scenarios, specially when
45+
no tests are collected, consider using the
46+
`pytest-custom_exit_code <https://github.com/yashtodi94/pytest-custom_exit_code>`__
47+
plugin.
48+
3749

3850
Getting help on version, option names, environment variables
3951
--------------------------------------------------------------

src/_pytest/assertion/rewrite.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -116,24 +116,11 @@ def exec_module(self, module):
116116
write = not sys.dont_write_bytecode
117117
cache_dir = os.path.join(os.path.dirname(fn), "__pycache__")
118118
if write:
119-
try:
120-
os.mkdir(cache_dir)
121-
except OSError:
122-
e = sys.exc_info()[1].errno
123-
if e == errno.EEXIST:
124-
# Either the __pycache__ directory already exists (the
125-
# common case) or it's blocked by a non-dir node. In the
126-
# latter case, we'll ignore it in _write_pyc.
127-
pass
128-
elif e in {errno.ENOENT, errno.ENOTDIR}:
129-
# One of the path components was not a directory, likely
130-
# because we're in a zip file.
131-
write = False
132-
elif e in {errno.EACCES, errno.EROFS, errno.EPERM}:
133-
state.trace("read only directory: %r" % os.path.dirname(fn))
134-
write = False
135-
else:
136-
raise
119+
ok = try_mkdir(cache_dir)
120+
if not ok:
121+
write = False
122+
state.trace("read only directory: {}".format(os.path.dirname(fn)))
123+
137124
cache_name = os.path.basename(fn)[:-3] + PYC_TAIL
138125
pyc = os.path.join(cache_dir, cache_name)
139126
# Notice that even if we're in a read-only directory, I'm going
@@ -1026,3 +1013,26 @@ def visit_Compare(self, comp):
10261013
else:
10271014
res = load_names[0]
10281015
return res, self.explanation_param(self.pop_format_context(expl_call))
1016+
1017+
1018+
def try_mkdir(cache_dir):
1019+
"""Attempts to create the given directory, returns True if successful"""
1020+
try:
1021+
os.mkdir(cache_dir)
1022+
except FileExistsError:
1023+
# Either the __pycache__ directory already exists (the
1024+
# common case) or it's blocked by a non-dir node. In the
1025+
# latter case, we'll ignore it in _write_pyc.
1026+
return True
1027+
except (FileNotFoundError, NotADirectoryError):
1028+
# One of the path components was not a directory, likely
1029+
# because we're in a zip file.
1030+
return False
1031+
except PermissionError:
1032+
return False
1033+
except OSError as e:
1034+
# as of now, EROFS doesn't have an equivalent OSError-subclass
1035+
if e.errno == errno.EROFS:
1036+
return False
1037+
raise
1038+
return True

src/_pytest/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,13 @@ def _collect(self, arg):
562562
# Module itself, so just use that. If this special case isn't taken, then all
563563
# the files in the package will be yielded.
564564
if argpath.basename == "__init__.py":
565-
yield next(m[0].collect())
565+
try:
566+
yield next(m[0].collect())
567+
except StopIteration:
568+
# The package collects nothing with only an __init__.py
569+
# file in it, which gets ignored by the default
570+
# "python_files" option.
571+
pass
566572
return
567573
yield from m
568574

src/_pytest/pathlib.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import atexit
2-
import errno
32
import fnmatch
43
import itertools
54
import operator
@@ -163,14 +162,8 @@ def create_cleanup_lock(p):
163162
lock_path = get_lock_path(p)
164163
try:
165164
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
166-
except OSError as e:
167-
if e.errno == errno.EEXIST:
168-
raise EnvironmentError(
169-
"cannot create lockfile in {path}".format(path=p)
170-
) from e
171-
172-
else:
173-
raise
165+
except FileExistsError as e:
166+
raise EnvironmentError("cannot create lockfile in {path}".format(path=p)) from e
174167
else:
175168
pid = os.getpid()
176169
spid = str(pid).encode()

testing/test_assertrewrite.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ast
2+
import errno
23
import glob
34
import importlib
45
import os
@@ -7,6 +8,7 @@
78
import sys
89
import textwrap
910
import zipfile
11+
from functools import partial
1012

1113
import py
1214

@@ -1528,3 +1530,43 @@ def test_simple():
15281530
)
15291531
def test_get_assertion_exprs(src, expected):
15301532
assert _get_assertion_exprs(src) == expected
1533+
1534+
1535+
def test_try_mkdir(monkeypatch, tmp_path):
1536+
from _pytest.assertion.rewrite import try_mkdir
1537+
1538+
p = tmp_path / "foo"
1539+
1540+
# create
1541+
assert try_mkdir(str(p))
1542+
assert p.is_dir()
1543+
1544+
# already exist
1545+
assert try_mkdir(str(p))
1546+
1547+
# monkeypatch to simulate all error situations
1548+
def fake_mkdir(p, *, exc):
1549+
assert isinstance(p, str)
1550+
raise exc
1551+
1552+
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=FileNotFoundError()))
1553+
assert not try_mkdir(str(p))
1554+
1555+
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=NotADirectoryError()))
1556+
assert not try_mkdir(str(p))
1557+
1558+
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=PermissionError()))
1559+
assert not try_mkdir(str(p))
1560+
1561+
err = OSError()
1562+
err.errno = errno.EROFS
1563+
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=err))
1564+
assert not try_mkdir(str(p))
1565+
1566+
# unhandled OSError should raise
1567+
err = OSError()
1568+
err.errno = errno.ECHILD
1569+
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=err))
1570+
with pytest.raises(OSError) as exc_info:
1571+
try_mkdir(str(p))
1572+
assert exc_info.value.errno == errno.ECHILD

testing/test_collection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,18 @@ def test_collect_pkg_init_and_file_in_args(testdir):
12031203
)
12041204

12051205

1206+
def test_collect_pkg_init_only(testdir):
1207+
subdir = testdir.mkdir("sub")
1208+
init = subdir.ensure("__init__.py")
1209+
init.write("def test_init(): pass")
1210+
1211+
result = testdir.runpytest(str(init))
1212+
result.stdout.fnmatch_lines(["*no tests ran in*"])
1213+
1214+
result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init))
1215+
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
1216+
1217+
12061218
@pytest.mark.skipif(
12071219
not hasattr(py.path.local, "mksymlinkto"),
12081220
reason="symlink not available on this platform",

0 commit comments

Comments
 (0)