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
8 changes: 6 additions & 2 deletions pcapkit/utilities/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,10 @@ def _importer_stacklevel() -> 'int':
level then runs past the outermost frame, and the warning is attributed to
``<sys>:0``. This counts frames as :func:`warnings.warn` does, and
:mod:`logging` does from Python 3.11; on earlier versions the log record is
attributed to a frame inside the import.
attributed to a frame inside the import. GraalPy names the bootstrap's frames
``<frozen _frozen_importlib>`` and ``<frozen _frozen_importlib_external>``,
which its :func:`warnings.warn` leaves out too, so those are not counted
either (:issue:`1963`).

"""
pcapkit = f'{os.path.sep}pcapkit{os.path.sep}'
Expand All @@ -493,7 +496,8 @@ def _importer_stacklevel() -> 'int':
frame = frame.f_back if frame is not None else None # the caller, level 1
while frame is not None:
filename = frame.f_code.co_filename
if not ('importlib' in filename and '_bootstrap' in filename):
graalpy_bootstrap = filename.startswith('<frozen _frozen_importlib')
if not ('importlib' in filename and ('_bootstrap' in filename or graalpy_bootstrap)):
level += 1
outermost = level
if pcapkit in filename:
Expand Down
9 changes: 7 additions & 2 deletions tests/corekit/test_sentinel_py_abc_1960_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
:class:`PyABCFallbackTests` runs the checks in a child interpreter that really
takes the fallback. It starts with ``-S``, so that no module but :mod:`io` has
imported :mod:`abc` yet, drops both from :data:`sys.modules`, blocks ``_abc``
and imports them again. The child reports whether :class:`abc.ABCMeta` is
(except on GraalPy, which takes the fallback unaided, #1963), and imports
them again. The child reports whether :class:`abc.ABCMeta` is
``_py_abc``'s and whether ``_collections_abc`` was already loaded, rather than
trusting the block. :class:`CModuleTests` pins that, under the C module, the
names let through for ``_py_abc`` are still refused.
Expand Down Expand Up @@ -60,7 +61,11 @@
preloaded = sorted(name for name in ('abc', 'io', '_collections_abc') if name in sys.modules)
for name in ('abc', 'io'):
sys.modules.pop(name, None)
sys.modules['_abc'] = None
# GraalPy's ``_abc`` holds only the two helpers its own ``_py_abc`` imports,
# so :mod:`abc` takes the fallback there unaided, and blocking ``_abc``
# would break ``_py_abc`` too (#1963).
if sys.implementation.name != 'graalpy':
sys.modules['_abc'] = None

import abc
import io # noqa: F401 # rebuilt on the fallback before anything uses it
Expand Down
8 changes: 8 additions & 0 deletions tests/test_tier_guard_xdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@
#: Test tier (#1958): checks the guard in xdist workers on each leg's interpreter.
__tier__ = 'ALL'

#: Why the nested-session probes skip GraalPy (#1963): each starts a whole
#: ``pytest -n`` session of fresh GraalPy interpreters, which took minutes a test
#: on the experimental leg while the other workers idled. What they pin is the
#: guard's xdist handling, which no interpreter changes, so the CPython legs cover it.
SKIP_NESTED_ON_GRAALPY = 'a nested pytest-xdist session is too slow on GraalPy (#1963)'

#: Capture name the probe module in :class:`XdistSubprocessTests` reads. Not
#: tracked by git -- see :func:`tests._tiers.committed_captures` -- so it is a
#: tier violation regardless of what ``make samples`` has built locally.
Expand Down Expand Up @@ -177,6 +183,7 @@ def _child_environ() -> 'dict[str, str]':

@unittest.skipUnless(importlib.util.find_spec('xdist') is not None,
'pytest-xdist not installed')
@unittest.skipIf(sys.implementation.name == 'graalpy', SKIP_NESTED_ON_GRAALPY)
class XdistSubprocessTests(unittest.TestCase):
"""End-to-end reproduction: a real violation, a real ``pytest`` subprocess.

Expand Down Expand Up @@ -300,6 +307,7 @@ def pytest_runtest_setup(item):

@unittest.skipUnless(importlib.util.find_spec('xdist') is not None,
'pytest-xdist not installed')
@unittest.skipIf(sys.implementation.name == 'graalpy', SKIP_NESTED_ON_GRAALPY)
class ProbeWorkerCrashTests(unittest.TestCase):
"""A worker dying mid-probe fails that one test, not the session (#1756).

Expand Down
11 changes: 10 additions & 1 deletion tests/utilities/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@
#: assertion is aimed there.
OWN_MODULE_PREFIX = 'pcapkit'

#: Whether a :exc:`UnicodeDecodeError` row's message is compared past the octet
#: and position it names. GraalPy's codecs word the reason their own way --
#: ``malformed input`` for the rows below -- so there the reason is left out (#1963).
DECODE_REASON_COMPARED = sys.implementation.name != 'graalpy'

#: ``(value, encoding, errors, outcome)`` for ``compat.unquote``, each as Python
#: 3.9's :func:`urllib.parse.unquote` has it: the text returned, or the exception
#: type and message raised. ``None`` keywords leave the defaults in place. Before
Expand Down Expand Up @@ -340,7 +345,11 @@ def assert_unquotes_as_python39(self, unquote: 'Callable[..., str]') -> None:
else:
with self.assertRaises(outcome[0]) as raised:
unquote(value, **kwargs)
self.assertEqual(str(raised.exception), outcome[1])
message, expected = str(raised.exception), outcome[1]
if outcome[0] is UnicodeDecodeError and not DECODE_REASON_COMPARED:
message = message.rpartition(': ')[0]
expected = expected.rpartition(': ')[0]
self.assertEqual(message, expected)

def test_unquote_decodes_bytes_as_python39_does(self) -> None:
"""From 3.9 the name is :func:`urllib.parse.unquote` itself (#1693)."""
Expand Down
42 changes: 42 additions & 0 deletions tests/utilities/test_config_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,5 +590,47 @@ def test_an_error_filter_makes_the_import_raise(self) -> None:
self.assertIn(f"ConfigWarning: PCAPKIT_{name}={text!r}", result.stderr)


class ImporterStacklevelTests(unittest.TestCase):
"""GitHub issue #1963: the importer's level leaves out the bootstrap, however it is named.

CPython names :mod:`importlib`'s bootstrap frames ``<frozen importlib._bootstrap>``
and ``<frozen importlib._bootstrap_external>``, GraalPy ``<frozen _frozen_importlib>``
and ``<frozen _frozen_importlib_external>``, and each one's :func:`warnings.warn`
leaves its own out. Counting GraalPy's ran the level past the outermost frame,
so the import-time warnings above named ``<sys>:0`` there.

"""

def level_under(self, bootstrap: str) -> int:
"""The level a :mod:`pcapkit` frame gets, imported through ``bootstrap`` frames.

The chain is that of an import: this test, a bootstrap frame, a package
body, a bootstrap frame, then the reporting module. Leaving the bootstrap
out, the test is the third frame.

"""
pcapkit_warnings = importlib.import_module('pcapkit.utilities.warnings')
namespace = {'_importer_stacklevel': getattr(pcapkit_warnings, '_importer_stacklevel')}
package = os.path.join(os.sep, 'site-packages', 'pcapkit', 'probe_1963.py')
reporter = ('def report():\n return _importer_stacklevel()\n'
'def body():\n return load_inner()\n')
importer = ('def load_inner():\n return report()\n'
'def load_outer():\n return body()\n')
exec(compile(reporter, package, 'exec'), namespace) # nosec: B102 # pylint: disable=exec-used
exec(compile(importer, bootstrap, 'exec'), namespace) # nosec: B102 # pylint: disable=exec-used
return namespace['load_outer']()

def test_each_interpreters_bootstrap_frames_are_left_out(self) -> None:
"""Both CPython's names and GraalPy's leave the test the third frame."""
for bootstrap in ('<frozen importlib._bootstrap>', '<frozen importlib._bootstrap_external>',
'<frozen _frozen_importlib>', '<frozen _frozen_importlib_external>'):
with self.subTest(bootstrap=bootstrap):
self.assertEqual(self.level_under(bootstrap), 3)

def test_any_other_frame_is_counted(self) -> None:
"""The control: frames named otherwise are counted, so the level is one further out."""
self.assertEqual(self.level_under('<string>'), 4)


if __name__ == '__main__':
unittest.main()
Loading