Skip to content
Merged
Changes from 1 commit
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
Next Next commit
Refine documentation in interpreter.rs
Improve InterpreterConfig with convenience methods (with_debug, add_path, add_paths), better documentation with working examples, refactored stdlib setup into focused functions, and comprehensive unit tests while maintaining 100% backward compatibility.
  • Loading branch information
77axel authored and youknowone committed Dec 31, 2025
commit 8034f51c57a0326cfca3e596af237cc64a49d314
287 changes: 230 additions & 57 deletions src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,49 @@ use rustpython_vm::{Interpreter, PyRef, Settings, VirtualMachine, builtins::PyMo

pub type InitHook = Box<dyn FnOnce(&mut VirtualMachine)>;

/// The convenient way to create [rustpython_vm::Interpreter] with stdlib and other stuffs.
/// The convenient way to create [rustpython_vm::Interpreter] with stdlib and other components.
///
/// Basic usage:
/// ```
/// let interpreter = rustpython::InterpreterConfig::new()
/// # Basic Usage
/// ```no_run
/// use rustpython::InterpreterConfig;
///
/// let interpreter = InterpreterConfig::new()
/// .init_stdlib()
/// .interpreter();
/// ```
///
/// To override [rustpython_vm::Settings]:
/// ```
/// # Override Settings
/// ```no_run
/// use rustpython_vm::Settings;
/// // Override your settings here.
/// use rustpython::InterpreterConfig;
///
/// let mut settings = Settings::default();
/// settings.debug = 1;
/// // You may want to add paths to `rustpython_vm::Settings::path_list` to allow import python libraries.
/// settings.path_list.push("Lib".to_owned()); // add standard library directory
/// settings.path_list.push("".to_owned()); // add current working directory
/// let interpreter = rustpython::InterpreterConfig::new()
/// // Add paths to allow importing Python libraries
/// settings.path_list.push("Lib".to_owned()); // standard library directory
/// settings.path_list.push("".to_owned()); // current working directory
///
/// let interpreter = InterpreterConfig::new()
/// .settings(settings)
/// .interpreter();
/// ```
///
/// To add native modules:
/// ```compile_fail
/// let interpreter = rustpython::InterpreterConfig::new()
/// # Add Native Modules
/// ```no_run
/// use rustpython::InterpreterConfig;
/// use rustpython_vm::{VirtualMachine, PyRef, builtins::PyModule};
///
/// fn make_custom_module(vm: &VirtualMachine) -> PyRef<PyModule> {
/// // Your module implementation
/// # todo!()
/// }
///
/// let interpreter = InterpreterConfig::new()
/// .init_stdlib()
/// .init_hook(Box::new(|vm| {
/// vm.add_native_module(
/// "your_module_name".to_owned(),
/// Box::new(your_module::make_module),
/// );
/// }))
/// .add_native_module(
/// "your_module_name".to_owned(),
/// make_custom_module,
/// )
/// .interpreter();
/// ```
#[derive(Default)]
Expand All @@ -44,9 +54,15 @@ pub struct InterpreterConfig {
}

impl InterpreterConfig {
/// Create a new interpreter configuration with default settings
pub fn new() -> Self {
Self::default()
}

/// Build the interpreter with the current configuration
///
/// # Panics
/// May panic if initialization hooks encounter fatal errors
pub fn interpreter(self) -> Interpreter {
let settings = self.settings.unwrap_or_default();
Interpreter::with_init(settings, |vm| {
Expand All @@ -56,14 +72,37 @@ impl InterpreterConfig {
})
}

/// Set custom settings for the interpreter
///
/// If called multiple times, only the last settings will be used
pub fn settings(mut self, settings: Settings) -> Self {
self.settings = Some(settings);
self
}

/// Add a custom initialization hook
///
/// Hooks are executed in the order they are added during interpreter creation
pub fn init_hook(mut self, hook: InitHook) -> Self {
self.init_hooks.push(hook);
self
}

/// Add a native module to the interpreter
///
/// # Arguments
/// * `name` - The module name that will be used for imports
/// * `make_module` - Function that creates the module when called
///
/// # Example
/// ```no_run
/// # use rustpython::InterpreterConfig;
/// # use rustpython_vm::{VirtualMachine, PyRef, builtins::PyModule};
/// # fn my_module(vm: &VirtualMachine) -> PyRef<PyModule> { todo!() }
/// let interpreter = InterpreterConfig::new()
/// .add_native_module("mymodule".to_owned(), my_module)
/// .interpreter();
/// ```
pub fn add_native_module(
self,
name: String,
Expand All @@ -73,56 +112,190 @@ impl InterpreterConfig {
vm.add_native_module(name, Box::new(make_module))
}))
}

/// Initialize the Python standard library
///
/// This adds all standard library modules to the interpreter.
/// Requires the `stdlib` feature to be enabled at compile time.
#[cfg(feature = "stdlib")]
pub fn init_stdlib(self) -> Self {
self.init_hook(Box::new(init_stdlib))
}

/// Initialize the Python standard library (no-op without stdlib feature)
///
/// When the `stdlib` feature is not enabled, this method does nothing
/// and prints a warning. Enable the `stdlib` feature to use the standard library.
#[cfg(not(feature = "stdlib"))]
pub fn init_stdlib(self) -> Self {
eprintln!(
"Warning: stdlib feature is not enabled. Standard library will not be available."
);
self
}

/// Convenience method to set the debug level
///
/// # Example
/// ```no_run
/// # use rustpython::InterpreterConfig;
/// let interpreter = InterpreterConfig::new()
/// .with_debug(1)
/// .interpreter();
/// ```
pub fn with_debug(mut self, level: u8) -> Self {
self.settings.get_or_insert_with(Default::default).debug = level;
self
}

/// Convenience method to add a single path to the module search paths
///
/// # Example
/// ```no_run
/// # use rustpython::InterpreterConfig;
/// let interpreter = InterpreterConfig::new()
/// .add_path("Lib")
/// .add_path(".")
/// .interpreter();
/// ```
pub fn add_path(mut self, path: impl Into<String>) -> Self {
self.settings
.get_or_insert_with(Default::default)
.path_list
.push(path.into());
self
}

/// Add multiple paths to the module search paths at once
///
/// # Example
/// ```no_run
/// # use rustpython::InterpreterConfig;
/// let interpreter = InterpreterConfig::new()
/// .add_paths(vec!["Lib", ".", "custom_modules"])
/// .interpreter();
/// ```
pub fn add_paths<I, S>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let settings = self.settings.get_or_insert_with(Default::default);
settings.path_list.extend(paths.into_iter().map(Into::into));
self
}
}

/// Initialize the standard library modules
///
/// This function sets up both native modules and handles frozen/dynamic stdlib loading
#[cfg(feature = "stdlib")]
pub fn init_stdlib(vm: &mut VirtualMachine) {
vm.add_native_modules(rustpython_stdlib::get_module_inits());

// if we're on freeze-stdlib, the core stdlib modules will be included anyway
#[cfg(feature = "freeze-stdlib")]
{
vm.add_frozen(rustpython_pylib::FROZEN_STDLIB);
setup_frozen_stdlib(vm);

#[cfg(not(feature = "freeze-stdlib"))]
setup_dynamic_stdlib(vm);
}

/// Setup frozen standard library
///
/// Used when the stdlib is compiled into the binary
#[cfg(all(feature = "stdlib", feature = "freeze-stdlib"))]
fn setup_frozen_stdlib(vm: &mut VirtualMachine) {
vm.add_frozen(rustpython_pylib::FROZEN_STDLIB);

// FIXME: Remove this hack once sys._stdlib_dir is properly implemented
// or _frozen_importlib doesn't depend on it anymore.
// The assert ensures _stdlib_dir doesn't already exist before we set it
assert!(vm.sys_module.get_attr("_stdlib_dir", vm).is_err());
vm.sys_module
.set_attr(
"_stdlib_dir",
vm.new_pyobj(rustpython_pylib::LIB_PATH.to_owned()),
vm,
)
.unwrap();
}

/// Setup dynamic standard library loading from filesystem
///
/// Used when the stdlib is loaded from disk at runtime
#[cfg(all(feature = "stdlib", not(feature = "freeze-stdlib")))]
fn setup_dynamic_stdlib(vm: &mut VirtualMachine) {
use rustpython_vm::common::rc::PyRc;

let state = PyRc::get_mut(&mut vm.state).unwrap();

// FIXME: Remove this hack once sys._stdlib_dir is properly implemented or _frozen_importlib doesn't depend on it anymore.
assert!(vm.sys_module.get_attr("_stdlib_dir", vm).is_err());
vm.sys_module
.set_attr(
"_stdlib_dir",
vm.new_pyobj(rustpython_pylib::LIB_PATH.to_owned()),
vm,
)
.unwrap();
let additional_paths = collect_stdlib_paths();

// Insert at the beginning so stdlib comes before user paths
for path in additional_paths.into_iter().rev() {
state.config.paths.module_search_paths.insert(0, path);
}
}

#[cfg(not(feature = "freeze-stdlib"))]
{
use rustpython_vm::common::rc::PyRc;

let state = PyRc::get_mut(&mut vm.state).unwrap();

// Collect additional paths to add
let mut additional_paths = Vec::new();

// BUILDTIME_RUSTPYTHONPATH should be set when distributing
if let Some(paths) = option_env!("BUILDTIME_RUSTPYTHONPATH") {
additional_paths.extend(
crate::settings::split_paths(paths)
.map(|path| path.into_os_string().into_string().unwrap()),
)
} else {
#[cfg(feature = "rustpython-pylib")]
additional_paths.push(rustpython_pylib::LIB_PATH.to_owned())
}

// Add to both path_list (for compatibility) and module_search_paths (for sys.path)
// Insert at the beginning so stdlib comes before user paths
for path in additional_paths.into_iter().rev() {
state.config.paths.module_search_paths.insert(0, path);
}
/// Collect standard library paths from build-time configuration
///
/// Checks BUILDTIME_RUSTPYTHONPATH environment variable or uses default pylib path
#[cfg(all(feature = "stdlib", not(feature = "freeze-stdlib")))]
fn collect_stdlib_paths() -> Vec<String> {
let mut additional_paths = Vec::new();

// BUILDTIME_RUSTPYTHONPATH should be set when distributing
if let Some(paths) = option_env!("BUILDTIME_RUSTPYTHONPATH") {
additional_paths.extend(
crate::settings::split_paths(paths)
.map(|path| path.into_os_string().into_string().unwrap()),
)
} else {
#[cfg(feature = "rustpython-pylib")]
additional_paths.push(rustpython_pylib::LIB_PATH.to_owned())
}

additional_paths
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_default_config() {
let config = InterpreterConfig::new();
assert!(config.settings.is_none());
assert!(config.init_hooks.is_empty());
}

#[test]
fn test_with_debug() {
let config = InterpreterConfig::new().with_debug(2);
let settings = config.settings.unwrap();
assert_eq!(settings.debug, 2);
}

#[test]
fn test_add_single_path() {
let config = InterpreterConfig::new().add_path("test/path");
let settings = config.settings.unwrap();
assert_eq!(settings.path_list.len(), 1);
assert_eq!(settings.path_list[0], "test/path");
}

#[test]
fn test_add_multiple_paths_sequential() {
let config = InterpreterConfig::new().add_path("path1").add_path("path2");
let settings = config.settings.unwrap();
assert_eq!(settings.path_list.len(), 2);
}

#[test]
fn test_add_paths_batch() {
let paths = vec!["path1", "path2", "path3"];
let config = InterpreterConfig::new().add_paths(paths);
let settings = config.settings.unwrap();
assert_eq!(settings.path_list.len(), 3);
}
}