p-e-w/heretic · error · ValueError

Import-based plugin must use the form 'fully.qualified.modul

Error message

Import-based plugin must use the form 'fully.qualified.module.ClassName'

What it means

The name had no ':' (so it is treated as an import path), but it contains no '.', meaning there is no way to split it into module and class parts. load_plugin() throws this to enforce the 'fully.qualified.module.ClassName' form for import-based plugins.

Source

Thrown at src/heretic/plugin.py:134

                    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:
            raise ValueError(
                "Import-based plugin must use the form 'fully.qualified.module.ClassName'"
            )
        module_name, class_name = name.rsplit(".", 1)
        try:
            module = importlib.import_module(module_name)
        except ImportError as e:
            raise ImportError(f"Error loading plugin '{name}': {e}") from e
        plugin_cls = validate_class(module, class_name)

    if not issubclass(plugin_cls, base_class):
        raise TypeError(f"Plugin '{name}' must subclass {base_class.__name__}")

    return plugin_cls


class Context:
    """
    Runtime context passed to plugins

View on GitHub (pinned to bedb94ef11)

Solutions

  1. Use the full dotted path including module, e.g. 'myproject.scorers.my_scorer.MyScorer'.
  2. Ensure the module is on sys.path (installed or PYTHONPATH set).
  3. If the class lives in a file, use the 'path/to/file.py:ClassName' form instead.

Example fix

# before
scorer = "KeywordRate"
# after
scorer = "heretic.scorers.keyword_rate.KeywordRate"
Defensive patterns

Strategy: validation

Validate before calling

def valid_import_plugin(name: str) -> bool:
    return "." in name and ":" not in name

Try / catch

try:
    cls = load_plugin(name, Scorer)
except ValueError as e:
    sys.exit(f"{e} (got '{name}')")

Prevention

When it happens

Trigger: load_plugin('MyScorer', Scorer) — just a class name; passing a bare top-level symbol expecting heretic to guess the module; config value missing the dotted module prefix.

Common situations: Users familiar with entry-point style plugins passing only a name; copying only the class portion out of documentation; abbreviating 'heretic.scorers.keyword_rate.KeywordRate' to 'KeywordRate'.

Related errors


AI-assisted analysis of p-e-w/heretic@bedb94ef11 (2026-08-29). Data as JSON: /api/errors/eae0127a2a075ebf. Report an issue: GitHub.