p-e-w/heretic · error · ImportError
Could not load plugin '{name}' (invalid module spec)
Error message
Could not load plugin '{name}' (invalid module spec) What it means
importlib.util.spec_from_file_location() returned no spec or no loader, so the plugin file cannot be turned into a module. load_plugin() raises this ImportError early rather than crashing with an opaque AttributeError later.
Source
Thrown at src/heretic/plugin.py:115
plugin_path = Path(file_path)
if not plugin_path.is_absolute():
plugin_path = Path.cwd() / plugin_path
plugin_path = plugin_path.resolve()
if not plugin_path.is_file():
raise ImportError(f"Plugin file '{plugin_path}' does not exist")
# We're writing directly to the sys.modules dict,
# so the typical restrictions on module names
# (no dots, slashes, etc.) don't apply.
module_name = f"heretic_plugin_{plugin_path}"
# Reuse already-loaded modules to avoid re-executing the plugin on repeated loads.
module = sys.modules.get(module_name)
if module is None:
spec = importlib.util.spec_from_file_location(module_name, plugin_path)
if spec is None or spec.loader is None:
raise ImportError(
f"Could not load plugin '{name}' (invalid module spec)"
)
module = importlib.util.module_from_spec(spec)
# Cache before executing to match normal import semantics and allow
# circular imports. If execution fails, remove the entry.
sys.modules[module_name] = module
try:
spec.loader.exec_module(module)
except Exception:
sys.modules.pop(module_name, None)
raise
plugin_cls = validate_class(module, class_name)
# Fully-qualified import path, e.g "heretic.scorers.keyword_rate.KeywordRate".
else:
if "." not in name:View on GitHub (pinned to bedb94ef11)
Solutions
- Confirm plugin_path.is_file() is True right before loading (race conditions are the usual cause).
- Verify the Python runtime supports SourceFileLoader for this path (standard CPython does for .py).
- Re-check that the path contains no invalid characters for the loader and retry with a plain, local .py file.
Example fix
# before (blind call)
plugin_cls = load_plugin(args.scorer, Scorer)
# after
target = Path(args.scorer.split(':',1)[0]).resolve()
if not target.is_file():
raise FileNotFoundError(f"missing plugin file: {target}")
plugin_cls = load_plugin(args.scorer, Scorer) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def assert_loadable(name: str) -> None:
target = Path(name.rsplit(":", 1)[0]).resolve()
if not target.is_file():
raise FileNotFoundError(target) Try / catch
try:
cls = load_plugin(name, Scorer)
except ImportError as e:
logging.error("plugin load failed: %s", e)
raise Prevention
- Re-stat the file immediately before loading to avoid races.
- Use plain local .py files on standard CPython.
When it happens
Trigger: spec_from_file_location returning None for the resolved plugin_path — typically a nonexistent file slipping through, an unsupported extension (non-.py already filtered, but e.g. broken symlinks or special files), or a loader mismatch on unusual filesystem entries.
Common situations: Files added to a cache/overlay that disappeared before import; permission or filesystem oddities in containers; Python builds lacking the source-file loader for the given path type.
Related errors
- Error loading plugin '{name}': {e}
- Plugin '{name}' does not export a class named '{class_name}'
- You must append the plugin class name to the filepath like t
- File-based plugin must use the form 'path/to/plugin.py:Class
- Import-based plugin must use the form 'fully.qualified.modul
AI-assisted analysis of p-e-w/heretic@bedb94ef11 (2026-08-29).
Data as JSON: /api/errors/205538d2bc1c5e76.
Report an issue: GitHub.