|
2 | 2 |
|
3 | 3 | use crate::{ |
4 | 4 | AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, |
5 | | - builtins::{PyCode, PyStr, list, traceback::PyTraceback}, |
| 5 | + builtins::{PyCode, PyStr, PyStrRef, list, traceback::PyTraceback}, |
6 | 6 | exceptions::types::PyBaseException, |
7 | 7 | scope::Scope, |
8 | 8 | vm::{VirtualMachine, resolve_frozen_alias, thread}, |
@@ -30,6 +30,7 @@ pub(crate) fn init_importlib_base(vm: &mut VirtualMachine) -> PyResult<PyObjectR |
30 | 30 | Ok(bootstrap) |
31 | 31 | })?; |
32 | 32 | vm.import_func = importlib.get_attr(identifier!(vm, __import__), vm)?; |
| 33 | + vm.importlib = importlib.clone(); |
33 | 34 | Ok(importlib) |
34 | 35 | } |
35 | 36 |
|
@@ -340,3 +341,235 @@ pub(crate) fn is_stdlib_module_name(name: &PyObjectRef, vm: &VirtualMachine) -> |
340 | 341 | let result = vm.call_method(&stdlib_names, "__contains__", (name.clone(),))?; |
341 | 342 | result.try_to_bool(vm) |
342 | 343 | } |
| 344 | + |
| 345 | +/// PyImport_ImportModuleLevelObject |
| 346 | +pub fn import_module_level( |
| 347 | + name: &PyStr, |
| 348 | + globals: Option<PyObjectRef>, |
| 349 | + fromlist: Option<PyObjectRef>, |
| 350 | + level: i32, |
| 351 | + vm: &VirtualMachine, |
| 352 | +) -> PyResult { |
| 353 | + if level < 0 { |
| 354 | + return Err(vm.new_value_error("level must be >= 0".to_owned())); |
| 355 | + } |
| 356 | + |
| 357 | + let name_str = name.as_str(); |
| 358 | + |
| 359 | + // Resolve absolute name |
| 360 | + let abs_name = if level > 0 { |
| 361 | + // When globals is not provided (Rust None), raise KeyError |
| 362 | + // matching resolve_name() where globals==NULL |
| 363 | + if globals.is_none() { |
| 364 | + return Err(vm.new_key_error( |
| 365 | + vm.ctx.new_str("'__name__' not in globals").into(), |
| 366 | + )); |
| 367 | + } |
| 368 | + let globals_ref = globals.as_ref().unwrap(); |
| 369 | + // When globals is Python None, treat like empty mapping |
| 370 | + let empty_dict_obj; |
| 371 | + let globals_ref = if vm.is_none(globals_ref) { |
| 372 | + empty_dict_obj = vm.ctx.new_dict().into(); |
| 373 | + &empty_dict_obj |
| 374 | + } else { |
| 375 | + globals_ref |
| 376 | + }; |
| 377 | + let package = calc_package(Some(globals_ref), vm)?; |
| 378 | + if package.is_empty() { |
| 379 | + return Err(vm.new_import_error( |
| 380 | + "attempted relative import with no known parent package".to_owned(), |
| 381 | + vm.ctx.new_str(""), |
| 382 | + )); |
| 383 | + } |
| 384 | + resolve_name(name_str, &package, level as usize, vm)? |
| 385 | + } else { |
| 386 | + if name_str.is_empty() { |
| 387 | + return Err(vm.new_value_error("Empty module name".to_owned())); |
| 388 | + } |
| 389 | + name_str.to_owned() |
| 390 | + }; |
| 391 | + |
| 392 | + // import_get_module + import_find_and_load |
| 393 | + let sys_modules = vm.sys_module.get_attr("modules", vm)?; |
| 394 | + let module = match sys_modules.get_item(&*abs_name, vm) { |
| 395 | + Ok(m) if !vm.is_none(&m) => m, |
| 396 | + _ => { |
| 397 | + let find_and_load = vm.importlib.get_attr("_find_and_load", vm)?; |
| 398 | + let abs_name_obj = vm.ctx.new_str(&*abs_name); |
| 399 | + find_and_load.call((abs_name_obj, vm.import_func.clone()), vm)? |
| 400 | + } |
| 401 | + }; |
| 402 | + |
| 403 | + // Handle fromlist |
| 404 | + let has_from = fromlist |
| 405 | + .as_ref() |
| 406 | + .filter(|fl| !vm.is_none(fl)) |
| 407 | + .and_then(|fl| fl.clone().try_to_bool(vm).ok()) |
| 408 | + .unwrap_or(false); |
| 409 | + |
| 410 | + if has_from { |
| 411 | + let fromlist = fromlist.unwrap(); |
| 412 | + // Only call _handle_fromlist if the module looks like a package |
| 413 | + // (has __path__). Non-module objects without __name__/__path__ would |
| 414 | + // crash inside _handle_fromlist; IMPORT_FROM handles per-attribute |
| 415 | + // errors with proper ImportError conversion. |
| 416 | + let has_path = vm |
| 417 | + .get_attribute_opt(module.clone(), vm.ctx.intern_str("__path__"))? |
| 418 | + .is_some(); |
| 419 | + if has_path { |
| 420 | + let handle_fromlist = vm.importlib.get_attr("_handle_fromlist", vm)?; |
| 421 | + handle_fromlist.call((module, fromlist, vm.import_func.clone()), vm) |
| 422 | + } else { |
| 423 | + Ok(module) |
| 424 | + } |
| 425 | + } else if level == 0 || !name_str.is_empty() { |
| 426 | + match name_str.find('.') { |
| 427 | + None => Ok(module), |
| 428 | + Some(dot) => { |
| 429 | + let to_return = if level == 0 { |
| 430 | + name_str[..dot].to_owned() |
| 431 | + } else { |
| 432 | + let cut_off = name_str.len() - dot; |
| 433 | + abs_name[..abs_name.len() - cut_off].to_owned() |
| 434 | + }; |
| 435 | + match sys_modules.get_item(&*to_return, vm) { |
| 436 | + Ok(m) => Ok(m), |
| 437 | + Err(_) if level == 0 => { |
| 438 | + // For absolute imports (level 0), try importing the |
| 439 | + // parent. Matches _bootstrap.__import__ behavior. |
| 440 | + let find_and_load = vm.importlib.get_attr("_find_and_load", vm)?; |
| 441 | + let to_return_obj = vm.ctx.new_str(&*to_return); |
| 442 | + find_and_load.call((to_return_obj, vm.import_func.clone()), vm) |
| 443 | + } |
| 444 | + Err(_) => { |
| 445 | + // For relative imports (level > 0), raise KeyError |
| 446 | + let to_return_obj: PyObjectRef = vm |
| 447 | + .ctx |
| 448 | + .new_str(format!( |
| 449 | + "'{to_return}' not in sys.modules as expected" |
| 450 | + )) |
| 451 | + .into(); |
| 452 | + Err(vm.new_key_error(to_return_obj)) |
| 453 | + } |
| 454 | + } |
| 455 | + } |
| 456 | + } |
| 457 | + } else { |
| 458 | + Ok(module) |
| 459 | + } |
| 460 | +} |
| 461 | + |
| 462 | +/// resolve_name in import.c - resolve relative import name |
| 463 | +fn resolve_name(name: &str, package: &str, level: usize, vm: &VirtualMachine) -> PyResult<String> { |
| 464 | + // Python: bits = package.rsplit('.', level - 1) |
| 465 | + // Rust: rsplitn(level, '.') gives maxsplit=level-1 |
| 466 | + let parts: Vec<&str> = package.rsplitn(level, '.').collect(); |
| 467 | + if parts.len() < level { |
| 468 | + return Err(vm.new_import_error( |
| 469 | + "attempted relative import beyond top-level package".to_owned(), |
| 470 | + vm.ctx.new_str(name), |
| 471 | + )); |
| 472 | + } |
| 473 | + // rsplitn returns parts right-to-left, so last() is the leftmost (base) |
| 474 | + let base = parts.last().unwrap(); |
| 475 | + if name.is_empty() { |
| 476 | + Ok(base.to_string()) |
| 477 | + } else { |
| 478 | + Ok(format!("{base}.{name}")) |
| 479 | + } |
| 480 | +} |
| 481 | + |
| 482 | +/// _calc___package__ - calculate package from globals for relative imports |
| 483 | +fn calc_package(globals: Option<&PyObjectRef>, vm: &VirtualMachine) -> PyResult<String> { |
| 484 | + let globals = globals.ok_or_else(|| { |
| 485 | + vm.new_import_error( |
| 486 | + "attempted relative import with no known parent package".to_owned(), |
| 487 | + vm.ctx.new_str(""), |
| 488 | + ) |
| 489 | + })?; |
| 490 | + |
| 491 | + let package = globals.get_item("__package__", vm).ok(); |
| 492 | + let spec = globals.get_item("__spec__", vm).ok(); |
| 493 | + |
| 494 | + if let Some(ref pkg) = package |
| 495 | + && !vm.is_none(pkg) |
| 496 | + { |
| 497 | + let pkg_str: PyStrRef = pkg.clone().downcast().map_err(|_| { |
| 498 | + vm.new_type_error("package must be a string".to_owned()) |
| 499 | + })?; |
| 500 | + // Warn if __package__ != __spec__.parent |
| 501 | + if let Some(ref spec) = spec |
| 502 | + && !vm.is_none(spec) |
| 503 | + && let Ok(parent) = spec.get_attr("parent", vm) |
| 504 | + && !pkg_str.is(&parent) |
| 505 | + && pkg_str |
| 506 | + .as_object() |
| 507 | + .rich_compare_bool(&parent, crate::types::PyComparisonOp::Ne, vm) |
| 508 | + .unwrap_or(false) |
| 509 | + { |
| 510 | + let parent_repr = parent |
| 511 | + .repr(vm) |
| 512 | + .map(|s| s.as_str().to_owned()) |
| 513 | + .unwrap_or_default(); |
| 514 | + let msg = format!( |
| 515 | + "__package__ != __spec__.parent ('{}' != {})", |
| 516 | + pkg_str.as_str(), |
| 517 | + parent_repr |
| 518 | + ); |
| 519 | + let warn = vm |
| 520 | + .import("_warnings", 0) |
| 521 | + .and_then(|w| w.get_attr("warn", vm)); |
| 522 | + if let Ok(warn_fn) = warn { |
| 523 | + let _ = warn_fn.call( |
| 524 | + ( |
| 525 | + vm.ctx.new_str(msg), |
| 526 | + vm.ctx.exceptions.deprecation_warning.to_owned(), |
| 527 | + ), |
| 528 | + vm, |
| 529 | + ); |
| 530 | + } |
| 531 | + } |
| 532 | + return Ok(pkg_str.as_str().to_owned()); |
| 533 | + } else if let Some(ref spec) = spec |
| 534 | + && !vm.is_none(spec) |
| 535 | + && let Ok(parent) = spec.get_attr("parent", vm) |
| 536 | + && !vm.is_none(&parent) |
| 537 | + { |
| 538 | + let parent_str: PyStrRef = parent.downcast().map_err(|_| { |
| 539 | + vm.new_type_error("package set to non-string".to_owned()) |
| 540 | + })?; |
| 541 | + return Ok(parent_str.as_str().to_owned()); |
| 542 | + } |
| 543 | + |
| 544 | + // Fall back to __name__ and __path__ |
| 545 | + let warn = vm.import("_warnings", 0).and_then(|w| w.get_attr("warn", vm)); |
| 546 | + if let Ok(warn_fn) = warn { |
| 547 | + let _ = warn_fn.call( |
| 548 | + ( |
| 549 | + vm.ctx.new_str("can't resolve package from __spec__ or __package__, falling back on __name__ and __path__"), |
| 550 | + vm.ctx.exceptions.import_warning.to_owned(), |
| 551 | + ), |
| 552 | + vm, |
| 553 | + ); |
| 554 | + } |
| 555 | + |
| 556 | + let mod_name = globals.get_item("__name__", vm).map_err(|_| { |
| 557 | + vm.new_import_error( |
| 558 | + "attempted relative import with no known parent package".to_owned(), |
| 559 | + vm.ctx.new_str(""), |
| 560 | + ) |
| 561 | + })?; |
| 562 | + let mod_name_str: PyStrRef = mod_name.downcast().map_err(|_| { |
| 563 | + vm.new_type_error("__name__ must be a string".to_owned()) |
| 564 | + })?; |
| 565 | + let mut package = mod_name_str.as_str().to_owned(); |
| 566 | + // If not a package (no __path__), strip last component. |
| 567 | + // Uses rpartition('.')[0] semantics: returns empty string when no dot. |
| 568 | + if globals.get_item("__path__", vm).is_err() { |
| 569 | + package = match package.rfind('.') { |
| 570 | + Some(dot) => package[..dot].to_owned(), |
| 571 | + None => String::new(), |
| 572 | + }; |
| 573 | + } |
| 574 | + Ok(package) |
| 575 | +} |
0 commit comments