{"record":{"id":"eae0127a2a075ebf","repo":"p-e-w/heretic","slug":"import-based-plugin-must-use-the-form-fully-quali","errorCode":null,"errorMessage":"Import-based plugin must use the form 'fully.qualified.module.ClassName'","messagePattern":"Import-based plugin must use the form 'fully\\.qualified\\.module\\.ClassName'","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/heretic/plugin.py","lineNumber":134,"sourceCode":"                    f\"Could not load plugin '{name}' (invalid module spec)\"\n                )\n\n            module = importlib.util.module_from_spec(spec)\n\n            # Cache before executing to match normal import semantics and allow\n            # circular imports. If execution fails, remove the entry.\n            sys.modules[module_name] = module\n            try:\n                spec.loader.exec_module(module)\n            except Exception:\n                sys.modules.pop(module_name, None)\n                raise\n\n        plugin_cls = validate_class(module, class_name)\n    # Fully-qualified import path, e.g \"heretic.scorers.keyword_rate.KeywordRate\".\n    else:\n        if \".\" not in name:\n            raise ValueError(\n                \"Import-based plugin must use the form 'fully.qualified.module.ClassName'\"\n            )\n        module_name, class_name = name.rsplit(\".\", 1)\n        try:\n            module = importlib.import_module(module_name)\n        except ImportError as e:\n            raise ImportError(f\"Error loading plugin '{name}': {e}\") from e\n        plugin_cls = validate_class(module, class_name)\n\n    if not issubclass(plugin_cls, base_class):\n        raise TypeError(f\"Plugin '{name}' must subclass {base_class.__name__}\")\n\n    return plugin_cls\n\n\nclass Context:\n    \"\"\"\n    Runtime context passed to plugins","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/p-e-w/heretic/blob/bedb94ef117a271532ac2058447fbc165d5051bd/src/heretic/plugin.py#L116-L152","documentation":"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.","triggerScenarios":"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.","commonSituations":"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'.","solutions":["Use the full dotted path including module, e.g. 'myproject.scorers.my_scorer.MyScorer'.","Ensure the module is on sys.path (installed or PYTHONPATH set).","If the class lives in a file, use the 'path/to/file.py:ClassName' form instead."],"exampleFix":"# before\nscorer = \"KeywordRate\"\n# after\nscorer = \"heretic.scorers.keyword_rate.KeywordRate\"","handlingStrategy":"validation","validationCode":"def valid_import_plugin(name: str) -> bool:\n    return \".\" in name and \":\" not in name","typeGuard":null,"tryCatchPattern":"try:\n    cls = load_plugin(name, Scorer)\nexcept ValueError as e:\n    sys.exit(f\"{e} (got '{name}')\")","preventionTips":["Always use fully-qualified dotted paths for import plugins.","Copy plugin names from documentation verbatim, including module prefix."],"tags":["plugin","python","configuration"],"backgroundTag":"plugin-import-path-malformed","analyzedSha":"bedb94ef117a271532ac2058447fbc165d5051bd","analyzedAt":"2026-08-29T08:38:06.692Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}