Skip to content
Open
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
13 changes: 11 additions & 2 deletions crates/capi/src/import.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use crate::util::CStrExt;
use crate::{PyObject, pystate::with_vm};
use core::ffi::c_char;
use rustpython_vm::builtins::{PyCode, PyDict, PyModule, PyStr};
use rustpython_vm::builtins::{PyCode, PyDict, PyModule, PyStr, PyTuple};
use rustpython_vm::import::import_code_obj;

#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let name = unsafe { (&*name).try_downcast_ref::<PyStr>(vm)? };
vm.import(name, 0)
let from_list = PyTuple::new_ref_typed(vec![vm.ctx.new_str("*")], &vm.ctx);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this correct change? I didn't check code yet, but looking not matching to documented behavior
https://docs.python.org/3/c-api/import.html#c.PyImport_Import

@bschoenmaeckers bschoenmaeckers Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Before this change the testcase failed. It returned the top level module instead of abc. Current implementation passes all tests so it may be correct.

vm.import_from(name, &from_list, 0)
Comment on lines +11 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository files matching import.rs:\n'
fd -a 'import\.rs$' . | sed 's#^\./##'

printf '\nFile outline:\n'
ast-grep outline crates/capi/src/import.rs --view compact || true

printf '\nRelevant source:\n'
cat -n crates/capi/src/import.rs

printf '\nSearch for import_from and PyImport_Import references:\n'
rg -n "import_from|PyImport_Import|PyImport_ImportModule|PyImport_Import\\(|new_ref_typed|__all__|__doc__" crates/capi src . --glob '*.rs' | head -200

Repository: RustPython/RustPython

Length of output: 12300


🌐 Web query:

CPython Python/import.c PyImport_ImportModuleNoBlock __all__ dummy "__doc__"

💡 Result:

PyImport_ImportModuleNoBlock was a C API function in CPython that historically provided a way to import modules without blocking on the global import lock [1][2]. Background and Functionality: In older versions of CPython, a global import lock was used to protect the module import process, which could lead to deadlocks in multi-threaded applications [2]. PyImport_ImportModuleNoBlock was introduced as a "clumsy workaround" to allow C extensions to import modules without waiting for this lock, raising an ImportError instead if the lock was held by another thread [3][2]. Changes in Python 3.3+: With the introduction of per-module import locks in Python 3.3, the original purpose of this function became obsolete [1][2]. Consequently, PyImport_ImportModuleNoBlock was refactored to simply act as an alias for PyImport_ImportModule [4][3][5]. Deprecation and Removal: Due to its redundancy, the function was deprecated in Python 3.13 and is scheduled for removal in Python 3.15 [1][5]. Developers are advised to use PyImport_ImportModule instead [1]. Regarding the specific terms in your query ("all", "dummy", "doc"): These are standard Python identifiers and attributes [6]. While they appear throughout the CPython codebase (often used for defining module exports or docstrings), they are not specifically or uniquely associated with the internal logic of PyImport_ImportModuleNoBlock [3][7]. The function's implementation in Python/import.c is straightforward, simply wrapping the standard import call [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Import implementation around import_from/import_code:\n'
sed -n '320,430p' crates/vm/src/import.rs | cat -n -v

printf '\nFrame import_from and __all__ handling:\n'
sed -n '6920,6985p' crates/vm/src/frame.rs | cat -n -v

printf '\nC API implementation around PyImport_Import calls:\n'
rg -n "PyImport_Import|Module_Lookup|Import_Import|fromlist|__all__" crates/vm/src crates/capi/src crates/stdlib/src --glob '*.rs' | head -200

printf '\nPython runtime behavior for wildcard/non-wildcard imports into a pkg with __all__:\n'
cat > /tmp/rustpython_wildcard_probe.py <<'PY'
import sys
import pathlib
sys.path.insert(0, str(pathlib.Path(__file__).parent))
import importlib.util
spec = importlib.util.find_spec('collections.abc')
target = pathlib.Path(spec.origin).parent
(target / '_wildcardpkg.py').write_text("import sys\n")
(target / '_nonwild.py').write_text("import sys\n")
(target / '_wildcardpkg' / 'mod1.py').write_text("import sys\n")
(target / '_nonwild' / 'mod1.py').write_text("import sys\n")
imp = __import__('_wildcardpkg', fromlist=['*'])
imp = __import__('_nonwild', fromlist=['__doc__'])
PY
python3 /tmp/rustpython_wildcard_probe.py || true

Repository: RustPython/RustPython

Length of output: 10480


🌐 Web query:

RustPython crates/vm/src/import.rs import_from from_list "*"

💡 Result:

In RustPython, import_from is a method on the VirtualMachine struct used to perform Python's "from... import..." statement, while from_list refers to the list of names being imported from the specified module. Technical Details: - The import_from method on VirtualMachine is designed to be equivalent to executing Python code like from module_name import item1, item2 [1]. - In the RustPython codebase (specifically within crates/vm/src/vm/mod.rs and the VirtualMachine implementation), from_list is typically represented as a tuple of strings (&Py<PyTuple<PyStrRef>>), representing the symbols to be imported from the target module [1][2]. - Internally, this often involves invoking the Python importlib machinery. The import system in RustPython is largely designed to leverage the standard Python importlib (a pure Python implementation of the import machinery) by bootstrapping it, rather than re-implementing the entire complex import logic in Rust [3]. - The import_from function signature generally requires the module_name, the from_list (a tuple of names to import), and a level (for relative imports, where 0 is an absolute import) [1][2]. - When bytecode is executed, the IMPORT_FROM opcode is used to retrieve an individual name from the imported module [3][4]. If you are working with the source code in crates/vm/src/import.rs, you will find utilities for handling package resolution, such as resolve_name for calculating relative paths and calc_package for determining the package context from globals [5]. The actual coordination between the Rust VM and the Python-level importlib is a core part of how these functions maintain compatibility with standard Python import semantics [5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Use a tmpdir outside site-packages so the probe can write files safely.
tmpdir="$(mktemp -d)"
cd "$tmpdir"

cat > probe.py <<'PY'
import sys
sys.path.insert(0, ".")

pathlib(__import__("pathlib").Path("pkg_star")).mkdir()
pathlib("pkg_star/mod1.py").write_text("")
pathlib("pkg_star").write_bytes(b"from . import mod1")

pkg = __import__("pkg_star", fromlist=["*"])
print("pkg_star.star_imported", "mod1" in sys.modules)

sys.modules.pop("pkg_star.mod1", None)

pathlib("pkg_nostar").mkdir()
pathlib("pkg_nostar/mod1.py").write_text("")
pathlib("pkg_nostar").write_bytes(b"from . import mod1")

pkg = __import__("pkg_nostar", fromlist=["__doc__"])
print("pkg_nostar.star_imported", "pkg_nostar.mod1" in sys.modules)
PY
python3 probe.py

Repository: RustPython/RustPython

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
cd "$tmpdir"

cat > probe.py <<'PY'
import sys, pathlib
pathlib.Path("pkg_star").mkdir(parents=True)
pathlib.Path("pkg_star/mod1.py").write_text("")
pathlib.Path("pkg_star/__init__.py").write_bytes(b"from . import mod1")

pkg = __import__("pkg_star", fromlist=["*"])
print("pkg_star.star_imported", "pkg_star.mod1" in sys.modules)

del sys.modules["pkg_star.mod1"]

pathlib.Path("pkg_nostar").mkdir(parents=True)
pathlib.Path("pkg_nostar/mod1.py").write_text("")
pathlib.Path("pkg_nostar/__init__.py").write_bytes(b"from . import mod1")

pkg = __import__("pkg_nostar", fromlist=["__doc__"])
print("pkg_nostar.star_imported", "pkg_nostar.mod1" in sys.modules)
PY
python3 probe.py

Repository: RustPython/RustPython

Length of output: 389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat > /tmp/python_import_check.py <<'PY'
import importlib.util
spec = importlib.util.find_spec("collections.abc")
print(spec.origin)
print(spec.submodule_search_locations)
PY
python3 /tmp/python_import_check.py

cat > /tmp/collections_abc_probe.py <<'PY'
import sys
pkg = __import__("collections.abc", fromlist=["__doc__"])
print("seq_loaded", "collections.abc.Sequence" in sys.modules)
print("abc_loaded", pkg is sys.modules["collections.abc"])
print("has Sequence", hasattr(pkg, "Sequence"))
PY
python3 /tmp/collections_abc_probe.py

Repository: RustPython/RustPython

Length of output: 255


🌐 Web query:

RustPython crates/vm/src/vm/mod.rs VirtualMachine import_from __doc__ fromlist

💡 Result:

In RustPython, the VirtualMachine::import_from method is a core function used to implement the Python import mechanism, specifically handling statements like from module_name import item1, item2 [1]. The method signature is defined as follows: pub fn import_from<'a>( &self, module_name: impl AsPyStr<'a>, from_list: &Py, level: usize,) -> PyResult Parameters: 1. module_name: The name of the module to import from [1]. 2. from_list: A tuple of strings (PyTuple) representing the names (or attributes) to be imported from the module [1]. This corresponds to the list of names specified after the import keyword in a Python from... import... statement [2]. 3. level: An integer indicating whether to use absolute or relative imports (0 for absolute, >0 for relative) [1][3]. Functionality: This method is roughly equivalent to the Python statement from module_name import item1, item2 [1]. Internally, it invokes the import system to locate the module and then extracts the requested symbols specified in the from_list [1][4]. The VirtualMachine struct is defined in crates/vm/src/vm/mod.rs [5], and the import logic frequently interacts with the importlib machinery initialized during the VM setup [4]. While VirtualMachine::import handles standard imports (import module_name), import_from provides the necessary mechanism for accessing specific objects from within a module [1].

Citations:


Use a non-wildcard sentinel for PyImport_Import.

PyImport_Import is a full-module import API, but passing ["*"] calls RustPython’s import_from path and treats it as a wildcard from ... import *, which expands package __all__ before returning the module. Prefer a dummy fromlist such as ["__doc__"] to avoid side effects while still avoiding the Python wildcard logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/import.rs` around lines 11 - 12, Update the from_list
sentinel in the PyImport_Import implementation to use a non-wildcard name such
as "__doc__" instead of "*". Keep the existing vm.import_from call and module
import behavior unchanged while preventing wildcard __all__ expansion.

})
}

Expand Down Expand Up @@ -74,4 +75,12 @@ mod tests {
let _module = py.import("types").unwrap();
})
}

#[test]
fn import_sub_module() {
Python::attach(|py| {
let module = py.import("collections.abc").unwrap();
module.getattr("Sequence").unwrap();
})
}
}
14 changes: 14 additions & 0 deletions crates/capi/src/methodobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub(crate) fn build_method_def(

let flags = PyMethodFlags::from_bits(ml.ml_flags as u32)
.ok_or_else(|| vm.new_system_error("PyMethodDef contains unknown flags"))?;
let has_self = has_self && !flags.contains(PyMethodFlags::STATIC);

let method = ml.ml_meth;

Expand Down Expand Up @@ -359,4 +360,17 @@ mod tests {
);
})
}

#[test]
fn wrap_static_no_args_function() {
#[pyfunction()]
fn f() {}

Python::attach(|py| {
let module = PyModule::new(py, "test_wrap_pyfunction_forms").unwrap();

let func = wrap_pyfunction!(f, &module).unwrap();
func.call0().unwrap();
});
}
}
16 changes: 14 additions & 2 deletions crates/capi/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ pub unsafe extern "C" fn PyObject_HasAttrWithError(
with_vm(|vm| {
let obj = unsafe { &*obj };
let name = unsafe { &*attr_name }.try_downcast_ref::<PyStr>(vm)?;
obj.has_attr(name, vm)
Ok(vm.get_attribute_opt(obj.to_owned(), name)?.is_some())
})
}

Expand Down Expand Up @@ -272,7 +272,7 @@ pub unsafe extern "C" fn PyObject_HasAttrStringWithError(
with_vm(|vm| {
let obj = unsafe { &*obj };
let name = unsafe { attr_name.try_as_str(vm) }?;
obj.has_attr(name, vm)
Ok(vm.get_attribute_opt(obj.to_owned(), name)?.is_some())
})
}

Expand Down Expand Up @@ -591,4 +591,16 @@ mod tests {
assert!(dict.get_item("foo").is_ok());
})
}

#[test]
fn hasattr() {
Python::attach(|py| {
let x = 5i32.into_pyobject(py).unwrap();
assert!(x.is_instance_of::<PyInt>());

// spell-checker:ignore bbbbbbytes
assert!(x.hasattr("to_bytes").unwrap());
assert!(!x.hasattr("bbbbbbytes").unwrap());
})
}
}
23 changes: 23 additions & 0 deletions crates/capi/src/pycapsule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ pub unsafe extern "C" fn PyCapsule_GetContext(capsule: *mut PyObject) -> *mut c_
let capsule = unsafe { &*capsule }
.downcast_ref_if_exact::<PyCapsule>(vm)
.ok_or_else(|| vm.new_value_error("Invalid capsule"))?;

if capsule.pointer().is_null() {
return Err(vm.new_value_error("Capsule has null pointer"));
}

Ok(capsule.context())
})
}
Expand Down Expand Up @@ -143,6 +148,7 @@ fn checked_capsule<'a>(

#[cfg(test)]
mod tests {
use pyo3::ffi;
use pyo3::prelude::*;
use pyo3::types::PyCapsule;

Expand All @@ -156,4 +162,21 @@ mod tests {
assert_eq!(unsafe { ptr.cast::<String>().as_ref() }, "Some data");
})
}

#[test]
fn capsule_context_on_invalid_capsule() {
Python::attach(|py| {
let cap = PyCapsule::new_with_value(py, 123u32, c"name").unwrap();

// Invalidate the capsule
// SAFETY: intentionally breaking the capsule for testing
unsafe {
ffi::PyCapsule_SetPointer(cap.as_ptr(), core::ptr::null_mut());
}

// context() on invalid capsule should fail
let result = cap.context();
assert!(result.is_err());
});
}
}
4 changes: 3 additions & 1 deletion crates/vm/src/builtins/capsule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ impl Representable for PyCapsule {

impl Destructor for PyCapsule {
fn del(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
if let Some(destructor) = zelf.destructor() {
if !zelf.pointer().is_null()
&& let Some(destructor) = zelf.destructor()
{
unsafe { destructor(zelf.as_object().as_raw().cast_mut()) };
}
Ok(())
Expand Down
Loading