Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
fba87be
feat: opt-in bundling of pure-Python third-party dependencies
tinovyatkin Aug 9, 2026
7875b88
fix: address PR review feedback for third-party bundling
tinovyatkin Aug 9, 2026
445e1a8
refactor: deduplicate third-party bundling integration test setup
tinovyatkin Aug 9, 2026
dd74fd1
fix: AST-based metadata detection and importlib static site-packages …
tinovyatkin Aug 9, 2026
fbbf156
chore: fix typo flagged by typos check (unparseable -> unparsable)
tinovyatkin Aug 9, 2026
ada7b6b
fix: detect importlib.resources usage and cache site-packages roots
tinovyatkin Aug 9, 2026
604c73e
fix: capture keyword package= context for static importlib imports
tinovyatkin Aug 9, 2026
e4f66cb
fix: RECORD-wide native detection, importlib_resources, entry-dir ven…
tinovyatkin Aug 9, 2026
bde7d8e
test: add snapshot fixture for third-party bundling; parse RECORD as CSV
tinovyatkin Aug 9, 2026
4ec89ce
fix: propagate Requires-Dist, enforce Requires-Python, keep dynamic i…
tinovyatkin Aug 9, 2026
69f01e3
fix: alias-aware dynamic imports, egg-info metadata, namespace owners…
tinovyatkin Aug 9, 2026
bab279a
fix: track egg-info fixture metadata despite global gitignore rule
tinovyatkin Aug 9, 2026
a8b118e
fix: merge requirement constraints and block __import__ in bundled deps
tinovyatkin Aug 9, 2026
7304713
fix: track fixture native .so artifacts despite global gitignore rule
tinovyatkin Aug 9, 2026
5f51349
fix: support keyword name= in import_module handling; evaluate extras…
tinovyatkin Aug 10, 2026
bf413d4
fix: shared import_module arg reader, extras-aware requirements, spec…
tinovyatkin Aug 10, 2026
76fcc36
test: cover alias-after-use ordering in dynamic import detection
tinovyatkin Aug 10, 2026
878e397
fix: recursively collect import_module aliases in the detection pre-pass
tinovyatkin Aug 10, 2026
511e37f
fix: assigned dynamic-import aliases, shared libraries, PYTHONPATH me…
tinovyatkin Aug 10, 2026
5ee98f6
fix: harden third-party bundling per review round
tinovyatkin Aug 10, 2026
f3ae20f
test: make interpreter-environment coverage hermetic against ambient …
tinovyatkin Aug 10, 2026
9a8a570
chore: backtick VIRTUAL_ENV in doc comment (clippy doc_markdown)
tinovyatkin Aug 10, 2026
6703286
fix: qualified __import__, literal metadata-API imports, extras union…
tinovyatkin Aug 10, 2026
810e6ad
fix: marker-branch merging, sysconfig interpreter query, header unfol…
tinovyatkin Aug 10, 2026
d59e915
fix: classify importlib targets before queuing, scope constraints to …
tinovyatkin Aug 10, 2026
bac2bcb
fix: preserve marker branches in final requirements merge; interprete…
tinovyatkin Aug 10, 2026
93a2242
chore: remove now-unused requirement_distribution_name helper
tinovyatkin Aug 10, 2026
c5e8055
fix: queue parent initializers for static submodule imports; ungate i…
tinovyatkin Aug 10, 2026
3ee5eb6
fix: exclude namespace parents from bundled ownership; keep plugin pr…
tinovyatkin Aug 10, 2026
c6bc1be
fix: track callables assigned from __import__ as undiscoverable
tinovyatkin Aug 10, 2026
4ba766c
fix: preserve double-bound import_module calls; patch-aware Requires-…
tinovyatkin Aug 10, 2026
d7b3e43
fix: keep queried-metadata providers and sourceless bytecode packages…
tinovyatkin Aug 10, 2026
69a3e2d
fix: capture keyword-bound metadata query names
tinovyatkin Aug 10, 2026
94bf16d
fix: index file-form egg-info metadata; clear run-specific discovery …
tinovyatkin Aug 10, 2026
35a52d9
fix: infer egg-info import roots from the distribution name
tinovyatkin Aug 10, 2026
b853e42
fix: record static targets dropped by late metadata queries
tinovyatkin Aug 10, 2026
d2051e0
fix: parse requirement strings in metadata query arguments
tinovyatkin Aug 10, 2026
4800ff3
fix: treat directory-entry iteration errors as uninspectable
tinovyatkin Aug 10, 2026
5815e1b
fix: carry over legacy egg-info requires.txt dependencies
tinovyatkin Aug 10, 2026
4ef4402
fix: treat unenumerable distribution metadata directories as unsafe
tinovyatkin Aug 10, 2026
3f0bc75
fix: respect local shadowing before rewriting import_module calls
tinovyatkin Aug 10, 2026
6956971
fix: treat unreadable distribution metadata files as unsafe
tinovyatkin Aug 10, 2026
e1ed210
fix: precompute function-local shadowing before rewriting calls
tinovyatkin Aug 10, 2026
06439a5
fix: keep __file__ resource consumers external
tinovyatkin Aug 10, 2026
cd938a8
fix: keep packages with escaping symlinked sources external
tinovyatkin Aug 10, 2026
32f8243
fix: prune dependencies orphaned by late reclassification
tinovyatkin Aug 10, 2026
0164d90
fix: preserve import_module calls with non-discardable arguments
tinovyatkin Aug 10, 2026
7f5f03e
fix: respect local shadowing when graphing import_module assignments
tinovyatkin Aug 10, 2026
1a6f7cd
fix: emit requirements for metadata-only distribution queries
tinovyatkin Aug 10, 2026
0c818fe
fix: harden third-party classification review findings
tinovyatkin Aug 10, 2026
2c394df
fix: preserve double-bound package arguments in import_module calls
tinovyatkin Aug 10, 2026
707395e
fix: metadata-query scoping, constraint preservation, requirement mer…
tinovyatkin Aug 10, 2026
5b56013
feat: bundle import_module targets with evaluable package arguments
tinovyatkin Aug 11, 2026
6f0c1e2
feat: register sys.modules self-accessing modules with import machinery
tinovyatkin Aug 11, 2026
09b4eba
fix: resolve sys and sys.modules aliases in self-access detection
tinovyatkin Aug 11, 2026
950df76
fix: scan definition-time metadata queries in the enclosing scope
tinovyatkin Aug 11, 2026
4fb495b
fix: unregister failed self-registering wrappers from sys.modules
tinovyatkin Aug 11, 2026
f82b5ce
fix: spec consumers, wrapped import callables, class-order aliases, b…
tinovyatkin Aug 11, 2026
5af0e11
fix: address seventh-round review findings by solving where solvable
tinovyatkin Aug 11, 2026
5b449d4
fix: eighth-round review findings — scanner scoping, preserved first-…
tinovyatkin Aug 11, 2026
1b0a717
fix: ninth-round review findings — lazy call-site init, resolution un…
tinovyatkin Aug 11, 2026
bbb5b39
fix: resolve preserved import_module calls through a sys.meta_path fi…
tinovyatkin Aug 12, 2026
b6fbf7b
fix: make the metadata-query scanner conservative by construction
tinovyatkin Aug 12, 2026
e32f92d
fix: harden dynamic-import detection and RECORD-less ownership
tinovyatkin Aug 12, 2026
644af3d
fix: model Python binding order in shadow tracking across all passes
tinovyatkin Aug 12, 2026
69f50b7
fix: Distribution lookups, runpy consumers, configured-interpreter pu…
tinovyatkin Aug 12, 2026
c58d57e
fix: def/class definitions rebind import aliases in the enclosing scope
tinovyatkin Aug 12, 2026
3b45173
fix: deleted aliases, __path__ readers, .pth hooks, resource-read tar…
tinovyatkin Aug 13, 2026
b6d613e
fix: keep PEP 420 namespace providers external together
tinovyatkin Aug 13, 2026
1412b91
fix: give bundled classes a picklable module identity
tinovyatkin Aug 13, 2026
934633f
fix: dynamic metadata access, getattr import callables, reload target…
tinovyatkin Aug 13, 2026
2e4c78e
fix: loader failure reset and picklable inlined class modules
tinovyatkin Aug 13, 2026
418c6b1
fix: fifth-round hardening — resolution, identity, and requirement fi…
tinovyatkin Aug 13, 2026
5ec5e1c
fix: sixth-round hardening — scope fidelity, loader robustness, const…
tinovyatkin Aug 13, 2026
57c049a
chore: fix unused qualifications flagged by all-features clippy
tinovyatkin Aug 13, 2026
ee792f0
fix: seventh-round hardening — dynamic targets, identity, lazy modules
tinovyatkin Aug 13, 2026
5829b83
fix: eighth-round hardening — scopes, state fidelity, sibling natives
tinovyatkin Aug 13, 2026
8794386
fix: ninth-round hardening — lambda scopes, decorated stamps, __doc__…
tinovyatkin Aug 13, 2026
e2dba0d
fix: ninth-round findings — hooks in control flow, inlined identity, …
tinovyatkin Aug 13, 2026
1107077
fix: make ancestor-virtualenv test environment-independent
tinovyatkin Aug 13, 2026
8384b46
fix: tenth-round hardening — symlink scans, ismodule, venv priority, …
tinovyatkin Aug 13, 2026
c4eb6db
fix: eleventh-round hardening — conditional aliases, receivers, extra…
tinovyatkin Aug 13, 2026
f9e98e5
fix: twelfth-round hardening — dunder rebinding, inspect keywords, en…
tinovyatkin Aug 13, 2026
e1fec64
fix: thirteenth-round hardening — globals(), getmodule, retrieved cal…
tinovyatkin Aug 14, 2026
55ead8b
fix: fourteenth-round hardening — sixteen findings across two reviews
tinovyatkin Aug 14, 2026
a078c4a
fix: gate symlinked-package-root test to unix
tinovyatkin Aug 14, 2026
8bdc35a
fix: fifteenth-round hardening — self identity, sibling roots, namesp…
tinovyatkin Aug 14, 2026
cef657b
fix: sixteenth-round hardening — parent-first resolution, exec, hasha…
tinovyatkin Aug 14, 2026
b079529
test: cover literal relative import anchored on __package__
tinovyatkin Aug 14, 2026
39dbdd2
fix: seventeenth-round hardening — eval/vars, annotations, weakref, v…
tinovyatkin Aug 14, 2026
b937816
fix: eighteenth-round hardening — comprehension/class scoping, decora…
tinovyatkin Aug 14, 2026
bcb0afa
fix: nineteenth-round hardening — external-dependent targets, conditi…
tinovyatkin Aug 14, 2026
f818fee
fix: twentieth-round hardening — conditional class methods, globals-b…
tinovyatkin Aug 14, 2026
ee2f615
fix: twenty-first-round hardening — relative-import __path__ semantic…
tinovyatkin Aug 14, 2026
fc1a6ea
fix: canonicalize relative-import package-dir resolution results
tinovyatkin Aug 14, 2026
06c38a5
fix: resolve bundled identities, reload semantics, and import-state s…
tinovyatkin Aug 15, 2026
3978f82
fix: import-machinery correctness for shadowing, reinit, and finder p…
tinovyatkin Aug 15, 2026
6da97ac
fix: sys.modules precedence, lazy-loader capture, class-body identity…
tinovyatkin Aug 15, 2026
25ac043
fix: complete runtime namespaces, preloaded parents, and destructured…
tinovyatkin Aug 15, 2026
f12ba00
fix: preloaded static imports, captured exports, nested stamps, dict …
tinovyatkin Aug 15, 2026
729bf3a
fix: conditional alias promotion, pristine bounds, extras edges, dete…
tinovyatkin Aug 16, 2026
20871c2
fix: relative preserved imports, real module objects, retries, walrus…
tinovyatkin Aug 16, 2026
c66a5d1
fix: loop else-state merges, match subject order, suite stamps, evict…
tinovyatkin Aug 16, 2026
3fd6e67
fix: metadata-query closure, namespace deferral, importlib dispatch p…
tinovyatkin Aug 16, 2026
fc8eea6
fix: regular-parent shadowing, split platlib policy, replaced parents…
tinovyatkin Aug 16, 2026
4c8060e
style: reword comment flagged by typos check
tinovyatkin Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ __pycache__/
build/
dist/
*.egg-info/
# Test fixtures simulate legacy installs and must keep their egg-info metadata
!crates/cribo/tests/fixtures/**/*.egg-info/
*.so
# Test fixtures simulate native-extension installs with fake .so artifacts
!crates/cribo/tests/fixtures/**/*.so
+*.whl
+venv/
+.venv/
Expand Down
30 changes: 28 additions & 2 deletions crates/cribo/src/analyzers/module_classifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,28 @@ impl<'a> ModuleClassifier<'a> {
let has_invalid_identifier =
!ruff_python_stdlib::identifiers::is_identifier(module_base_name);

if has_side_effects || has_invalid_identifier || needs_wrapping_for_circular {
// Modules inspecting sys.modules (e.g. `sys.modules[__name__]`, even
// inside function bodies) need a real module object registered under
// their original name: the wrapper approach provides one and its init
// registers it in sys.modules. Targets of preserved import_module calls
// need the same registration so the runtime call resolves them, and so
// do modules whose sys.modules entry a CONSUMER observes
// (`sys.modules[dep.__name__]`). ANCESTOR packages of preserved dotted
// targets are imported by the machinery too (parents load before
// children), so they need a real init for reload/eviction semantics.
let accesses_own_sys_modules =
crate::visitors::utils::accesses_own_sys_modules_entry(&ast.body)
|| self.resolver.is_preserved_importlib_target(&module_name)
|| self
.resolver
.is_preserved_importlib_target_ancestor(&module_name)
|| self.resolver.is_sys_modules_observed_target(&module_name);
Comment on lines +218 to +224

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep modules that mutate their module class external

When a pure dependency installs a custom module type with sys.modules[__name__].__class__ = CustomModule, this condition treats the self-access as safely handled by the wrapper path. The wrapper namespace is a types.SimpleNamespace, however, so assigning a ModuleType subclass to its __class__ raises TypeError because the object layouts differ, whereas the installed module supports the custom properties or lookup behavior. Use a real module object for this pattern or keep such providers external, with an execution snapshot.

AGENTS.md reference: AGENTS.md:L245-L250

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

✅ Addressed in 55ead8b. Thanks! (Apologies for the delayed reply on this round — it was processed together with the follow-up review.)


if has_side_effects
|| has_invalid_identifier
|| needs_wrapping_for_circular
|| accesses_own_sys_modules
{
if has_invalid_identifier {
debug!(
"Module '{module_name}' has invalid Python identifier - using wrapper \
Expand All @@ -216,8 +237,13 @@ impl<'a> ModuleClassifier<'a> {
debug!(
"Module '{module_name}' is in circular dependency - using wrapper approach"
);
} else {
} else if has_side_effects {
debug!("Module '{module_name}' has side effects - using wrapper approach");
} else {
debug!(
"Module '{module_name}' inspects sys.modules - using wrapper approach so \
it is registered under its original name"
);
}

wrapper_modules.push((
Expand Down
66 changes: 66 additions & 0 deletions crates/cribo/src/ast_builder/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,72 @@ pub(crate) fn bool_op(op: BoolOp, values: Vec<Expr>) -> Expr {
})
}

/// Creates the runtime identity guard for stamping a DECORATED definition:
///
/// ```python
/// getattr(<binding>, '__name__', None) == '<expected_name>' and
/// getattr(<binding>, '__module__', None) == (lambda: None).__module__
/// ```
///
/// A decorator may return an unrelated object — in particular an IMPORTED
/// shared callable whose `__name__` happens to equal the decorated binding.
/// Such an object must keep its defining module's attribution, so the guard
/// requires BOTH the definition's `__name__` AND creation provenance: a
/// locally created result (the original `def`, or a `functools.wraps`-style
/// copy of it) carries the creating scope's module name, which the inline
/// lambda probe reproduces exactly (both draw it from the enclosing
/// `globals()['__name__']` at execution time), while an imported callable
/// carries its own module's name (bundled modules stamp it right after the
/// definition, before any importer can observe the object).
pub(crate) fn decorated_identity_guard(binding: &str, expected_name: &str) -> Expr {
use ruff_python_ast::ExprLambda;

let name_check = compare(
call(
name("getattr", ExprContext::Load),
vec![
name(binding, ExprContext::Load),
string_literal("__name__"),
none_literal(),
],
vec![],
),
CmpOp::Eq,
string_literal(expected_name),
);
let probe = Expr::Lambda(ExprLambda {
node_index: AtomicNodeIndex::NONE,
range: TextRange::default(),
parameters: None,
body: Box::new(none_literal()),
});
let provenance_check = compare(
call(
name("getattr", ExprContext::Load),
vec![
name(binding, ExprContext::Load),
string_literal("__module__"),
none_literal(),
],
vec![],
),
CmpOp::Eq,
attribute(probe, "__module__", ExprContext::Load),
);
bool_op(BoolOp::And, vec![name_check, provenance_check])
}

/// Creates a comparison expression node with a single operator.
fn compare(left: Expr, op: CmpOp, right: Expr) -> Expr {
Expr::Compare(ExprCompare {
node_index: AtomicNodeIndex::NONE,
range: TextRange::default(),
left: Box::new(left),
ops: Box::new([op]),
comparators: Box::new([right]),
})
}

/// Creates an if-expression (ternary conditional) node.
///
/// # Arguments
Expand Down
3 changes: 3 additions & 0 deletions crates/cribo/src/ast_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ pub(crate) mod expressions;
pub(crate) mod module_attr_merge;
pub(crate) mod module_wrapper;
pub(crate) mod other;
pub(crate) mod preserved_finder;
pub(crate) mod proxy_generator;
pub(crate) mod statements;

/// Internal namespace prefix used for hoisted stdlib access.
pub(crate) const CRIBO_PREFIX: &str = "_cribo";
/// Alias under which the proxy prelude imports `sys` (`import sys as _sys`).
pub(crate) const CRIBO_SYS_ALIAS: &str = "_sys";

// Re-export commonly used functions for convenience
110 changes: 109 additions & 1 deletion crates/cribo/src/ast_builder/module_wrapper.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ruff_python_ast::{ExprContext, Stmt};
use ruff_python_ast::{Expr, ExprContext, Stmt};

use crate::{
ast_builder::{expressions, statements},
Expand Down Expand Up @@ -72,6 +72,22 @@ pub(crate) fn create_wrapper_module(
);
stmts.push(namespace_stmt);

// 1b. Capture the namespace OBJECT in the meta-path finder: runtime imports
// must resolve it even when user code later rebinds the bundle-global name
// (`globals()["helper"] = sentinel` before a preserved import)
stmts.push(statements::expr(expressions::call(
expressions::attribute(
expressions::name("_cribo_finder", ExprContext::Load),
"bind",
ExprContext::Load,
Comment on lines +80 to +82

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 Keep wrapper namespaces from overwriting the finder

When a wrapped module sanitizes to _cribo_finder—for example, a side-effectful _cribo_finder.py—the namespace assignment immediately above overwrites the generated finder object, and this call then raises AttributeError while constructing the bundle. Reserve the finder identifier or store it under a collision-proof generated name before emitting wrapper namespace bindings.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

✅ Addressed in 06c38a5: sanitize_module_name_for_identifier() now reserves all generated bundle globals (_cribo, _cribo_finder, _cribo_finder_local, _sys, _importlib, _Cribo, _CriboModule, _CriboPreservedFinder, _CriboPreservedLoader), suffixing colliding module-derived names so wrapper namespaces can never clobber the finder infrastructure. Thanks!

),
vec![
expressions::string_literal(module_name),
expressions::name(&module_var, ExprContext::Load),
],
vec![],
)));

// 2. Add the init function definition and __init__ assignment if provided
if let Some(init_body) = init_function_body {
let init_stmts = create_init_function_statements(module_name, init_func_name, init_body);
Expand Down Expand Up @@ -108,3 +124,95 @@ pub(crate) fn create_wrapper_module_init_call(module_name: &str) -> Stmt {
),
)
}

/// Wrap a bundled-module access expression with a `sys.modules` consult:
///
/// ```python
/// _cribo.importlib.import_module("pkg.sub") \
/// if "pkg" in _cribo.sys.modules or "pkg.sub" in _cribo.sys.modules \
/// else <access>
/// ```
///
/// CPython's `_find_and_load` returns an existing `sys.modules` entry before
/// invoking any finder or loader, and resolves DOTTED names through the
/// parent's `__path__` — so a preloaded replacement of the target OR of any
/// ancestor package must route through the real machinery rather than the
/// bundled access. When no component is preloaded, the direct bundled access
/// is used. The conditional (rather than `or`) also honors falsy replacement
/// objects.
pub(crate) fn sys_modules_consult_or(module_name: &str, access: Expr) -> Expr {
use ruff_python_ast::BoolOp;

let cribo_attribute = |attribute: &str| {
expressions::attribute(
expressions::name(super::CRIBO_PREFIX, ExprContext::Load),
attribute,
ExprContext::Load,
)
};
let sys_modules =
|| expressions::attribute(cribo_attribute("sys"), "modules", ExprContext::Load);

// "pkg" in _cribo.sys.modules or "pkg.mid" in ... or "pkg.mid.sub" in ...
let mut component_tests = Vec::new();
let mut boundary = 0_usize;
loop {
match module_name[boundary..].find('.') {
Some(offset) => boundary += offset,
None => boundary = module_name.len(),
}
component_tests.push(expressions::in_op(
expressions::string_literal(&module_name[..boundary]),
sys_modules(),
));
if boundary == module_name.len() {
break;
}
boundary += 1;
}
let any_component_preloaded = if component_tests.len() == 1 {
component_tests
.pop()
.expect("one component test must exist")
} else {
expressions::bool_op(BoolOp::Or, component_tests)
};

expressions::if_exp(
any_component_preloaded,
expressions::call(
expressions::attribute(
cribo_attribute("importlib"),
"import_module",
ExprContext::Load,
),
vec![expressions::string_literal(module_name)],
vec![],
),
access,
)
}

/// Creates a wrapper module initialization call that honors preloaded
/// `sys.modules` entries, for modules whose entries consumer code observably
/// manipulates:
///
/// `module = <consult> else module.__init__(module)`
pub(crate) fn create_wrapper_module_init_call_honoring_sys_modules(
module_var: &str,
original_module_name: &str,
) -> Stmt {
let init_call = expressions::call(
expressions::attribute(
expressions::name(module_var, ExprContext::Load),
MODULE_INIT_ATTR,
ExprContext::Load,
),
vec![expressions::name(module_var, ExprContext::Load)],
vec![],
);
statements::assign(
vec![expressions::name(module_var, ExprContext::Store)],
sys_modules_consult_or(original_module_name, init_call),
)
}
Loading
Loading