{"record":{"id":"efe7bb4ecbea67f2","repo":"p-e-w/heretic","slug":"file-based-plugin-must-use-the-form-path-to-plugi","errorCode":null,"errorMessage":"File-based plugin must use the form 'path/to/plugin.py:ClassName'","messagePattern":"File-based plugin must use the form 'path/to/plugin\\.py:ClassName'","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/heretic/plugin.py","lineNumber":93,"sourceCode":"        \"\"\"\n        obj = getattr(module, class_name, None)\n        if not inspect.isclass(obj):\n            raise ValueError(\n                f\"Plugin '{name}' does not export a class named '{class_name}'\"\n            )\n        return obj\n\n    # Common user trap with filepath imports.\n    if name.endswith(\".py\"):\n        raise ValueError(\n            \"You must append the plugin class name to the filepath like this: path/to/plugin.py:ClassName\"\n        )\n\n    # File path with explicit class name, e.g. \"C:\\\\path\\\\plugin.py:MyPlugin\".\n    if \":\" in name:\n        file_path, class_name = name.rsplit(\":\", 1)\n        if not file_path.endswith(\".py\") or not class_name:\n            raise ValueError(\n                \"File-based plugin must use the form 'path/to/plugin.py:ClassName'\"\n            )\n\n        plugin_path = Path(file_path)\n        if not plugin_path.is_absolute():\n            plugin_path = Path.cwd() / plugin_path\n        plugin_path = plugin_path.resolve()\n\n        if not plugin_path.is_file():\n            raise ImportError(f\"Plugin file '{plugin_path}' does not exist\")\n\n        # We're writing directly to the sys.modules dict,\n        # so the typical restrictions on module names\n        # (no dots, slashes, etc.) don't apply.\n        module_name = f\"heretic_plugin_{plugin_path}\"\n\n        # Reuse already-loaded modules to avoid re-executing the plugin on repeated loads.\n        module = sys.modules.get(module_name)","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/p-e-w/heretic/blob/bedb94ef117a271532ac2058447fbc165d5051bd/src/heretic/plugin.py#L75-L111","documentation":"A name containing ':' was given, but either the part before ':' does not end in '.py' or the class part after ':' is empty. load_plugin() throws this to enforce the strict 'path/to/plugin.py:ClassName' syntax for file-based plugins.","triggerScenarios":"load_plugin('plugins/my_scorer.txt:MyScorer', ...) with a non-.py extension; load_plugin('plugins/my_scorer.py:', ...) with an empty class name; Windows-style drive letters only in the segment (rare, handled by rsplit(':',1) but a truncated path like 'C:plugin.py:Foo' can slip through).","commonSituations":"Pointing the config at a compiled/other file type; leaving a trailing ':' after deleting the class name; typos like '..py' or '.pyy'.","solutions":["Make sure the path segment ends with '.py'.","Ensure a non-empty class name follows the ':'.","Verify with Path(file).is_file() that the target exists before running."],"exampleFix":"# before\nscorer = \"plugins/my_scorer.py:\"\n# after\nscorer = \"plugins/my_scorer.py:MyScorer\"","handlingStrategy":"validation","validationCode":"import re\nFILE_PLUGIN_RE = re.compile(r\"^.+\\.py:.+$\")\ndef valid_file_plugin(name: str) -> bool:\n    return \":\" in name and bool(FILE_PLUGIN_RE.match(name))","typeGuard":null,"tryCatchPattern":"try:\n    cls = load_plugin(name, Scorer)\nexcept ValueError:\n    sys.exit(\"use path/to/plugin.py:ClassName (non-empty .py path and class name)\")","preventionTips":["Check the file ends in .py and the class name after ':' is non-empty.","Avoid extra colons in paths on Windows except the single separator before ClassName."],"tags":["plugin","python","configuration"],"backgroundTag":"plugin-path-missing-class-name","analyzedSha":"bedb94ef117a271532ac2058447fbc165d5051bd","analyzedAt":"2026-08-29T08:38:06.692Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}