use crate::{
exceptions::{ExcType, MontyException},
object::CallArgs,
results::ExtFunctionResult,
type_checking::is_identifier,
};
pub const MAX_SEARCH_DIRS: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(from = "DumpedImportArgs")]
pub struct ImportArgs {
pub module: String,
pub search_path: Vec<String>,
}
impl ImportArgs {
#[must_use]
pub fn new(module: impl Into<String>) -> Self {
Self {
module: module.into(),
search_path: Vec::new(),
}
}
#[must_use]
pub fn from_call_args(args: &CallArgs) -> Option<Self> {
let mut args = args.args();
let module = args.next()?.as_str()?.to_owned();
let search_path = match args.next() {
Some(dirs) => dirs
.items()?
.iter()
.map(|dir| dir.as_str().map(str::to_owned))
.collect::<Option<_>>()?,
None => Vec::new(),
};
args.next().is_none().then_some(Self { module, search_path })
}
pub fn candidates(&self) -> impl Iterator<Item = String> + '_ {
let valid = self.module.split('.').all(is_identifier);
let leaf = self.module.rsplit('.').next().unwrap_or(&self.module);
self.search_path
.iter()
.filter(move |dir| valid && dir.starts_with('/') && !dir.contains('\0'))
.take(MAX_SEARCH_DIRS)
.flat_map(move |dir| {
let dir = dir.strip_suffix('/').unwrap_or(dir);
[format!("{dir}/{leaf}/__init__.py"), format!("{dir}/{leaf}.py")]
})
}
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum DumpedImportArgs {
Name(String),
Args {
module: String,
search_path: Vec<String>,
},
}
impl From<DumpedImportArgs> for ImportArgs {
fn from(dumped: DumpedImportArgs) -> Self {
match dumped {
DumpedImportArgs::Name(module) => Self::new(module),
DumpedImportArgs::Args { module, search_path } => Self { module, search_path },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ModuleFile {
pub path: String,
pub source: String,
}
#[derive(Debug)]
pub enum ModuleRead {
Text(String),
Missing,
NotHandled,
Error(MontyException),
}
impl ModuleRead {
#[must_use]
pub fn from_exception(exc: MontyException) -> Self {
match exc.exc_type() {
ExcType::FileNotFoundError
| ExcType::NotADirectoryError
| ExcType::IsADirectoryError
| ExcType::PermissionError => Self::Missing,
_ => Self::Error(exc),
}
}
#[must_use]
pub fn from_result(result: ExtFunctionResult) -> Self {
match result {
ExtFunctionResult::Return(value) => match value.as_ref().as_str() {
Some(text) => Self::Text(text.to_owned()),
None => Self::Error(MontyException::new(
ExcType::RuntimeError,
Some(format!(
"invalid return type: Path.read_text for an import must return a str, not {}",
value.as_ref().type_name()
)),
)),
},
ExtFunctionResult::Error(exc) => Self::from_exception(exc),
ExtFunctionResult::NotFound(_) => Self::NotHandled,
ExtFunctionResult::Future(_) | ExtFunctionResult::ModuleFile(_) => Self::Error(MontyException::new(
ExcType::RuntimeError,
Some("Path.read_text for an import must be answered with a str".to_owned()),
)),
}
}
}
pub fn find_module_file(args: &ImportArgs, mut read: impl FnMut(&str) -> ModuleRead) -> Option<ExtFunctionResult> {
for path in args.candidates() {
match read(&path) {
ModuleRead::Text(source) => return Some(ExtFunctionResult::ModuleFile(ModuleFile { path, source })),
ModuleRead::Missing | ModuleRead::NotHandled => {}
ModuleRead::Error(exc) => return Some(ExtFunctionResult::Error(exc)),
}
}
None
}