Skip to content

import: test_importlib compatibility and bootstrap-traceback parity fixes - #758

Merged
youknowone merged 11 commits into
mainfrom
import
Jul 23, 2026
Merged

import: test_importlib compatibility and bootstrap-traceback parity fixes#758
youknowone merged 11 commits into
mainfrom
import

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Continues the import/stdlib compatibility work after #746, targeting the CPython test_importlib suite plus PyPy code-level parity. 11 commits across import, module, io, marshal, posix, and the JIT walk gate.

Import / module

  • strip importlib bootstrap frames from import-error tracebacks — the slow-path __import__ result runs a frame stripper that drops the leading traceback entries whose code filename is importlib/_bootstrap{,_external}.py (or the frozen pseudo-names), stopping at the first non-bootstrap frame. Mirrors remove_traceback_module_frames (interp_import.py). import nosuch now shows only the user line; an erroring imported module keeps its own frames.
  • fall back to type __getattr__ when a replaced __getattribute__ raises — a module whose __getattribute__ slot is replaced (e.g. importlib.util._LazyModule) and raises AttributeError now routes to the receiver type's __getattr__ per descroperation.py, not the module-dict PEP 562 hook.
  • serve the default frozen table at override mode 0_override_frozen_modules_for_tests mode 0 (default) serves the whole frozen table; only a negative mode narrows to the essential bootstrap set.
  • trace w_class in the module GC shapeW_MODULE_GC_PTR_OFFSETS now traces ob_header.w_class so a ModuleType subclass instance's collectible class type is not swept when the module is its only referent.
  • install the zipimporter path hook after the bootstrap — adds zipimport.zipimporter to sys.path_hooks once the bootstrap is installed.

imp / marshal / io / posix / pyrex

  • imp.source_hash implemented with siphash-2-4.
  • marshal.loads/load accept any readable buffer.
  • io dispatches close dynamically from the context-manager __exit__.
  • posix.chmod implemented.
  • pyrex accepts the -u flag.

JIT

  • treat str and bytes as immutable inplace receivers in the walk gate — the inplace-op allowlist admits str/bytes receivers.

Verification

  • pyre/check.py: dynasm 300/300, cranelift 300/300.
  • Traceback strip, module w_class GC survival, frozen mode 0, and module __getattr__ fallback each checked against the CPython oracle on both backends.
  • test_importlib sweep: no regressions (remaining per-module failures are the fixture-precaching artifact, Windows-only winreg, or pre-existing edge cases).

🤖 Generated with Claude Code

IOBase.__exit__ and the buffered __exit__ called the native close
directly, so a Python subclass close() override never ran inside a
with-block — zipfile's _ZipWriteFile.close() was skipped and the
archive kept its open-writing-handle state. Dispatch self.close()
dynamically, the shape iobase_del already uses.

Assisted-by: Claude
install_importlib_bootstrap now inserts zipimport.zipimporter at the
front of the live sys.path_hooks list once the external importers are
installed, so zip archives on sys.path are importable. A failed
zipimport import leaves the hook out instead of failing the bootstrap.

Assisted-by: Claude
loads/load only took bytes and bytearray; a sliced memoryview — what
SourcelessFileLoader.get_code passes for the pyc payload — raised
TypeError. Route non-bytes inputs through buffer_as_bytes_like.

Assisted-by: Claude
The stub returned int 0, so _code_to_hash_pyc's len(source_hash) == 8
assert failed when writing hash-based pycs. Port interp_imp.py
source_hash: siphash-2-4 keyed by the pyc magic, 8 bytes little-endian.

Assisted-by: Claude
The inplace-BinaryOp journal gate declined exact str/bytes receivers
into InplaceContainerMutationUnsupported, a permanent abort whose
resume path drops the in-flight FOR_ITER item — posixpath.join lost
one path component once per process when its loop hit the compile
threshold mid-call. str/bytes += yields a fresh object and rebinds the
journaled local, the same argument as the existing int/bool/float/tuple
arm.

Assisted-by: Claude
script_helper spawns children with sys.executable -E -u; the parser
rejected -u with exit 2. pyre's stdio wrappers already write through
to the fd on every call, so the flag is accepted as a no-op.

Assisted-by: Claude
os.chmod was a silent no-op stub, so os_helper.can_chmod() observed an
unchanging st_mode and skipped chmod-dependent tests. Register a real
path-based chmod next to fchmod; the sandbox name set already
neutralises it.

Assisted-by: Claude
Add the `w_class` slot to `W_MODULE_GC_PTR_OFFSETS` so a Module keeps its
class reachable. For a `types.ModuleType` subclass instance the class is a
collectible heap `W_TypeObject`; when the module is its only referent, an
untraced slot let a major collection sweep the class and left `type(m)` and
slot dispatch pointing at freed memory.

Assisted-by: Claude
`frozen_module_served` treated mode 0 the same as a negative override, so at
the default `_override_frozen_modules_for_tests` setting only the essential
bootstrap set was served. Serve the whole frozen table when mode is
non-negative; a negative mode still keeps only the essential bootstrap set.

Assisted-by: Claude
…_ raises

When a module's `__getattribute__` slot is replaced (for example
importlib.util._LazyModule) and the replacement raises AttributeError, route
the miss to the receiver type's `__getattr__` per descroperation.py:242-245
instead of the module-dict PEP 562 `__getattr__`; re-raise when the type has
no `__getattr__`.

Assisted-by: Claude
`dunder_import` now runs `strip_bootstrap_traceback_frames` on the slow-path
`__import__` result, dropping the leading traceback entries whose code
filename belongs to `importlib/_bootstrap{,_external}.py` (or the frozen
pseudo-names). The walk stops at the first non-bootstrap frame, so an
erroring imported module keeps its own frames. Mirrors
`remove_traceback_module_frames` at interp_import.py:98.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ec78f8a2-dc35-45ad-a831-9281165268e9

📥 Commits

Reviewing files that changed from the base of the PR and between 2e72dab and e5dcb4b.

📒 Files selected for processing (9)
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/module/_io/mod.rs
  • pyre/pyre-interpreter/src/module/imp/interp_imp.rs
  • pyre/pyre-interpreter/src/module/marshal/mod.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-object/src/module.rs
  • pyre/pyrex/src/lib.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch import

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5dcb4b3d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return Err(crate::PyError::type_error("chmod() requires 2 arguments"));
}
let path = extract_path(args[0])?;
let mode = (unsafe { pyre_object::w_int_get_value(args[1]) }) as u32;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate chmod's mode before calling libc

In non-sandbox builds this new os.chmod path reads args[1] with w_int_get_value even though the wrapper only checks arity, so calls such as os.chmod(path, "0644") or any non-int mode reinterpret that object's layout as a W_IntObject and still invoke libc::chmod with an arbitrary mode instead of raising TypeError. Convert the mode through the object-space integer/index helper before the syscall so bad input cannot change permissions unpredictably.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit e5dcb4b).
Updated: 2026-07-23T19:00:57.990Z

Files in the reviewed diff
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/importing.rs
pyre/pyre-interpreter/src/module/_io/mod.rs
pyre/pyre-interpreter/src/module/imp/interp_imp.rs
pyre/pyre-interpreter/src/module/marshal/mod.rs
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-object/src/module.rs
pyre/pyrex/src/lib.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-interpreter/src/module/imp/interp_imp.rs:126 ↔ pypy/module/imp/interp_imp.py:117: default override 0 now serves every entry in Pyre’s expanded frozen table (mode >= 0), whereas PyPy’s default path only retains the explicit bootstrap list. This newly exposes test frozen modules such as __hello__ by default.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/importing.rs:2640 ↔ pypy/module/_frozen_importlib/interp_import.py:101: traceback stripping omits '<builtin>/frozen importlib._bootstrap_external', which PyPy removes alongside the two <frozen ...> names.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:2633 ↔ pypy/module/posix/interp_posix.py:1199: chmod is registered as a fixed two-argument builtin, so it cannot implement PyPy’s keyword-only dir_fd and follow_symlinks parameters.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:2639 ↔ pypy/module/posix/interp_posix.py:1227: PyPy accepts an integer file descriptor for chmod; the new implementation always routes argument one through path conversion and therefore rejects that supported form.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:2640 ↔ pypy/module/posix/interp_posix.py:1197: mode is read with the unchecked w_int_get_value instead of gateway integer coercion. PyPy’s c_int unwrap validates/converts the argument; non-int inputs can be misread rather than raising the required TypeError.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:2643 ↔ pypy/module/posix/interp_posix.py:1220: the direct libc::chmod call does not retry EINTR, unlike PyPy’s retry loop.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:1191 ↔ rpython/jit/metainterp/pyjitpl.py:2023: is_str/is_bytes are subtype checks, so a mutable-effect str or bytes subclass overriding __iadd__ is treated as immutable and excluded from rollback journaling. RPython executes residual calls through the executor without this subtype-based immutable exemption.

3. Pre-existing mismatches (already present before this patch)

None.

4. Structural adaptations

  • pyre/pyre-interpreter/src/importing.rs:2245 ↔ pypy/module/zipimport/moduledef.py:27: Pyre explicitly inserts zipimporter into sys.path_hooks during importlib bootstrap, replacing PyPy’s mixed-module startup() hook. This is a bootstrap-ownership adaptation with equivalent intended ordering.

  • pyre/pyre-interpreter/src/module/marshal/mod.rs:55 ↔ pypy/module/marshal/interp_marshal.py:456: Pyre obtains readable-buffer bytes through buffer_as_bytes_like and copies them, corresponding to PyPy’s space.buffer_w(...).as_readbuf() interface.

  • pyre/pyre-interpreter/src/module/imp/interp_imp.rs:424 ↔ pypy/module/imp/interp_imp.py:164: Pyre additionally accepts generic readable buffers for _imp.source_hash; PyPy’s older gateway declaration specifies bytes. This accommodates the CPython-compatible import path.

  • pyre/pyre-object/src/module.rs:52 ↔ pypy/interpreter/module.py:22: tracing ob_header.w_class is a Rust GC-layout requirement for collectible module subclasses; PyPy’s object model owns class reachability differently.

  • pyre/pyrex/src/lib.rs:201 ↔ pypy/interpreter/app_main.py:463: Pyre accepts -u without changing buffering, relying on its write-through stdio wrappers; PyPy configures raw and text streams as unbuffered/write-through.

@youknowone
youknowone merged commit 8fa227c into main Jul 23, 2026
29 of 31 checks passed
@youknowone
youknowone deleted the import branch July 23, 2026 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant