pub mod base64;
pub mod collections;
pub mod copy_mod;
pub mod dataclasses;
pub mod datetime;
#[path = "decimal_mod.rs"]
pub mod decimal;
#[path = "enum_mod.rs"]
pub mod enum_mod;
pub mod fractions;
pub mod functools;
pub mod hashlib;
pub mod itertools;
pub mod json;
pub mod math;
pub mod re;
pub mod statistics;
pub mod string;
pub mod textwrap;
pub mod typing;
use std::{collections::HashMap, sync::LazyLock};
use async_trait::async_trait;
use indexmap::IndexMap;
use rustpython_parser::ast;
use crate::{
error::{EvalError, EvalResult, InterpreterError},
state::InterpreterState,
tools::Tools,
value::{ExceptionValue, Value},
};
#[async_trait]
pub trait Module: Sync + Send {
fn name(&self) -> &'static str;
fn constant(&self, _name: &str) -> Option<Value> {
None
}
fn has_function(&self, _name: &str) -> bool {
false
}
async fn call(
&self,
state: &mut InterpreterState,
func: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
tools: &Tools,
) -> EvalResult {
let _ = (state, args, kwargs, tools);
Err(InterpreterError::AttributeError(format!(
"module '{}' has no callable '{func}'",
self.name(),
))
.into())
}
}
static MODULES: LazyLock<HashMap<&'static str, &'static dyn Module>> = LazyLock::new(|| {
let modules: [&'static dyn Module; 18] = [
&math::MathModule,
&json::JsonModule,
&re::ReModule,
&datetime::DatetimeModule,
&statistics::StatisticsModule,
&collections::CollectionsModule,
&string::StringModule,
&textwrap::TextwrapModule,
&base64::Base64Module,
&hashlib::HashlibModule,
&itertools::ItertoolsModule,
&functools::FunctoolsModule,
&typing::TypingModule,
&enum_mod::EnumModule,
&dataclasses::DataclassesModule,
&decimal::DecimalModule,
&fractions::FractionsModule,
©_mod::CopyModule,
];
modules.into_iter().map(|m| (m.name(), m)).collect()
});
const AUTO_IMPORTED: &[&str] = &["json", "re", "datetime"];
#[must_use]
pub fn is_known_module(name: &str) -> bool {
MODULES.contains_key(name)
}
#[must_use]
pub fn is_auto_imported(name: &str) -> bool {
AUTO_IMPORTED.contains(&name)
}
pub fn eval_import(state: &mut InterpreterState, node: &ast::StmtImport) -> EvalResult {
for alias in &node.names {
let module = alias.name.as_str();
if !is_known_module(module) {
return Err(module_not_found(module));
}
if module.contains('.') {
return Err(InterpreterError::Security(
"dotted/submodule imports are not supported".into(),
)
.into());
}
let bind = alias.asname.as_ref().map_or(module, rustpython_parser::ast::Identifier::as_str);
state
.set_variable(bind, Value::Module(module.to_string()))
.map_err(EvalError::Interpreter)?;
}
Ok(Value::None)
}
pub fn eval_import_from(state: &mut InterpreterState, node: &ast::StmtImportFrom) -> EvalResult {
if node.level.is_some_and(|level| level.to_u32() > 0) {
return Err(InterpreterError::Security(
"relative imports are not supported (see CONFORMANCE.md#import-allowlist)".into(),
)
.into());
}
let module =
node.module.as_ref().map(rustpython_parser::ast::Identifier::as_str).ok_or_else(|| {
EvalError::from(InterpreterError::Security(
"relative imports are not supported (see CONFORMANCE.md#import-allowlist)".into(),
))
})?;
if !is_known_module(module) {
return Err(module_not_found(module));
}
for alias in &node.names {
let name = alias.name.as_str();
if name == "*" {
return Err(InterpreterError::Security(
"`from module import *` is not supported".into(),
)
.into());
}
let value = module_member(module, name)?;
let bind = alias.asname.as_ref().map_or(name, rustpython_parser::ast::Identifier::as_str);
state.set_variable(bind, value).map_err(EvalError::Interpreter)?;
}
Ok(Value::None)
}
pub fn module_member(module: &str, name: &str) -> EvalResult {
if let Some(value) = constant(module, name) {
return Ok(value);
}
if has_function(module, name) {
return Ok(Value::ModuleFunction { module: module.to_string(), name: name.to_string() });
}
Err(InterpreterError::AttributeError(format!("module '{module}' has no attribute '{name}'"))
.into())
}
pub async fn call_function(
state: &mut crate::state::InterpreterState,
module: &str,
func: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
tools: &crate::tools::Tools,
) -> EvalResult {
let handler = MODULES.get(module).ok_or_else(|| module_not_found(module))?;
handler.call(state, func, args, kwargs, tools).await
}
fn constant(module: &str, name: &str) -> Option<Value> {
MODULES.get(module)?.constant(name)
}
fn has_function(module: &str, name: &str) -> bool {
MODULES.get(module).is_some_and(|m| m.has_function(name))
}
fn module_not_found(module: &str) -> EvalError {
EvalError::Exception(ExceptionValue::new(
"ModuleNotFoundError",
format!("No module named '{module}'"),
))
}
pub(crate) fn need_arg<'a>(
func: &str,
args: &'a [Value],
index: usize,
) -> Result<&'a Value, EvalError> {
args.get(index).ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(format!(
"{func}() missing required argument at position {index}"
)))
})
}
pub(crate) fn arg_f64(func: &str, args: &[Value], index: usize) -> Result<f64, EvalError> {
need_arg(func, args, index)?.as_float().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(format!(
"{func}() expected a number at position {index}"
)))
})
}
pub(crate) fn arg_str<'a>(
func: &str,
args: &'a [Value],
index: usize,
) -> Result<&'a str, EvalError> {
need_arg(func, args, index)?.as_str().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(format!(
"{func}() expected a string at position {index}"
)))
})
}
pub(crate) fn value_error(message: impl Into<String>) -> EvalError {
InterpreterError::ValueError(message.into()).into()
}
pub(crate) fn type_error(message: impl Into<String>) -> EvalError {
InterpreterError::TypeError(message.into()).into()
}
pub(crate) fn overflow_error(message: impl Into<String>) -> EvalError {
typed_exception("OverflowError", message)
}
pub(crate) fn statistics_error(message: impl Into<String>) -> EvalError {
typed_exception("statistics.StatisticsError", message)
}
pub(crate) fn json_decode_error(message: impl Into<String>) -> EvalError {
typed_exception("json.decoder.JSONDecodeError", message)
}
fn typed_exception(type_name: &str, message: impl Into<String>) -> EvalError {
ExceptionValue::new(type_name, message).into()
}
pub(crate) async fn call_callable(
state: &mut InterpreterState,
callable: &Value,
args: &[Value],
kwargs: &IndexMap<String, Value>,
tools: &crate::tools::Tools,
) -> EvalResult {
let _ = kwargs;
crate::eval::functions::call_value_as_function(state, callable, args, tools).await
}