{"record":{"id":"0e1f19f27ab572f7","repo":"p-e-w/heretic","slug":"you-must-append-the-plugin-class-name-to-the-filep","errorCode":null,"errorMessage":"You must append the plugin class name to the filepath like this: path/to/plugin.py:ClassName","messagePattern":"You must append the plugin class name to the filepath like this: path/to/plugin\\.py:ClassName","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/heretic/plugin.py","lineNumber":85,"sourceCode":"    - `path/to/plugin.py:MyPluginClass` (relative or absolute): load `MyPluginClass`\n      from that file.\n    - `fully.qualified.module.MyPluginClass`: import the module and load the class.\n    \"\"\"\n\n    def validate_class(module: ModuleType, class_name: str) -> type[Any]:\n        \"\"\"\n        Checks that the module actually exports the class as claimed and returns the class.\n        \"\"\"\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\")","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/p-e-w/heretic/blob/bedb94ef117a271532ac2058447fbc165d5051bd/src/heretic/plugin.py#L67-L103","documentation":"load_plugin() rejects bare '.py' file paths because a file alone cannot identify which class inside it to load. The library throws this to force the explicit 'path/to/plugin.py:ClassName' form required by its loader.","triggerScenarios":"Passing 'plugins/my_scorer.py' (no ':' and no class suffix) to load_plugin or a config/CLI field that resolves to a scorer plugin; using a path copied from an old version of the tool where the class name was inferred.","commonSituations":"Config written for an older heretic version where file plugins took just a path; forgetting the ':ClassName' suffix after migrating from directory-based plugin discovery; shell-quoting split the name at ':'.","solutions":["Append ':ClassName' to the path, where ClassName is the plugin class defined in that file.","Quote the argument in your shell so the ':' is not mangled.","If you meant an importable package plugin, use the dotted form 'pkg.module.ClassName' instead."],"exampleFix":"# before\nheretic --scorer plugins/my_scorer.py\n# after\nheretic --scorer plugins/my_scorer.py:MyScorer","handlingStrategy":"validation","validationCode":"def is_valid_file_plugin(name: str) -> bool:\n    return \":\" in name and name.split(\":\", 1)[0].endswith(\".py\") and name.endswith(\":\") is False and bool(name.rsplit(\":\", 1)[1])","typeGuard":"def looks_like_file_plugin(name: str) -> bool:\n    return name.endswith(\".py\") is False and \":\" in name and bool(name.rsplit(\":\", 1)[1])","tryCatchPattern":"try:\n    cls = load_plugin(name, Scorer)\nexcept ValueError as e:\n    sys.exit(f\"Plugin spec must be path/to/file.py:ClassName — got '{name}'\")","preventionTips":["Always write file plugin refs as 'path.py:ClassName'.","Quote args in shell so ':' is preserved.","Note bare '.py' paths are explicitly rejected by design."],"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"}