{"id":"5ed05a3a6bdfacd7","repo":"pypa/pip","slug":"invalid-script-entry-point-entry-point-a-call","errorCode":null,"errorMessage":"Invalid script entry point: {entry_point} - A callable suffix is required. See https://packaging.python.org/specifications/entry-points/#use-for-scripts for more information.","messagePattern":"Invalid script entry point: (.+?) - A callable suffix is required\\. See https://packaging\\.python\\.org/specifications/entry-points/#use-for-scripts for more information\\.","errorType":"exception","errorClass":"MissingCallableSuffix","httpStatus":null,"severity":"error","filePath":"src/pip/_internal/operations/install/wheel.py","lineNumber":403,"sourceCode":"\n\nclass MissingCallableSuffix(InstallationError):\n    def __init__(self, entry_point: str) -> None:\n        super().__init__(\n            f\"Invalid script entry point: {entry_point} - A callable \"\n            \"suffix is required. See https://packaging.python.org/\"\n            \"specifications/entry-points/#use-for-scripts for more \"\n            \"information.\"\n        )\n\n\ndef _raise_for_invalid_entrypoint(specification: str, scripts_dir: str) -> None:\n    entry = get_export_entry(specification)\n    if entry is None:\n        return\n\n    if entry.suffix is None:\n        raise MissingCallableSuffix(str(entry))\n\n    # distlib joins the entry point name onto the scripts directory, so a name\n    # with path separators or ``..`` components can resolve elsewhere. The script\n    # must resolve to a path strictly inside the scripts directory.\n    dest = os.path.join(scripts_dir, entry.name)\n    resolves_to_scripts_dir = os.path.abspath(dest) == os.path.abspath(scripts_dir)\n    if resolves_to_scripts_dir or not is_within_directory(scripts_dir, dest):\n        raise InstallationError(\n            f\"Invalid script entry point name {entry.name!r}: the script \"\n            f\"would be installed outside the scripts directory ({scripts_dir}).\"\n        )\n\n\nclass PipScriptMaker(ScriptMaker):\n    # Override distlib's default script template with one that\n    # doesn't import `re` module, allowing scripts to load faster.\n    script_template = textwrap.dedent(\"\"\"\\\n        import sys","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_internal/operations/install/wheel.py#L385-L421","documentation":"Raised as MissingCallableSuffix (an InstallationError subclass) when a console_scripts/gui_scripts entry point in a wheel is missing the 'module:callable' portion. At wheel.py:402-403, _raise_for_invalid_entrypoint parses the spec with distlib's get_export_entry; if the parsed ExportEntry has suffix is None (e.g. the spec is 'foo' with no '= module:func'), pip aborts the install rather than generating a broken script.","triggerScenarios":"A wheel declares an entry point like 'mypkg' (bare name) or 'mypkg =' (no callable) in [project.scripts] / [console_scripts]. The entry resolves via get_export_entry to a name with no suffix, tripping the check before script generation.","commonSituations":"A broken setup.py/setup.cfg/pyproject.toml where [console_scripts] omits the callable, or a hand-edited entry_points.txt. Seen when a maintainer writes 'mycli' instead of 'mycli = mypkg.cli:main', or after a botched sdist-to-wheel build.","solutions":["Inspect the offending wheel's entry_points.txt (or *.dist-info METADATA) to find the malformed entry point.","Fix the source declaration: ensure every script maps to 'name = package.module:object' (with the colon and callable).","Rebuild the wheel and reinstall: pip wheel . --no-deps && pip install dist/<pkg>-*.whl.","If installing someone else's broken wheel, pin to a known-good version or open an upstream issue."],"exampleFix":"# before (pyproject.toml)\n[project.scripts]\nmycli = \"mypkg.cli\"\n\n# after\n[project.scripts]\nmycli = \"mypkg.cli:main\"","handlingStrategy":"validation","validationCode":"from pip._vendor.distlib.util import get_export_entry\nimport zipfile, configparser, io\n\ndef validate_wheel_entrypoints(whl):\n    with zipfile.ZipFile(whl) as z:\n        for name in z.namelist():\n            if name.endswith(\"entry_points.txt\"):\n                cp = configparser.ConfigParser()\n                cp.read_string(z.read(name).decode())\n                for section in (\"console_scripts\", \"gui_scripts\"):\n                    for ep_name, spec in cp.items(section):\n                        entry = get_export_entry(f\"{ep_name} = {spec}\")\n                        if entry is None or entry.suffix is None:\n                            raise ValueError(f\"entry point '{ep_name}' missing callable: {spec}\")","typeGuard":"def has_callable_suffix(spec: str) -> bool:\n    from pip._vendor.distlib.util import get_export_entry\n    entry = get_export_entry(spec)\n    return entry is not None and entry.suffix is not None","tryCatchPattern":null,"preventionTips":["Lint [project.scripts] entries in CI to ensure 'name = module:callable' form.","Validate wheels before publishing: check every entry_point has a colon and callable.","Use a PEP 621-compliant pyproject.toml with a modern backend."],"tags":["wheel","entry-points","packaging","install"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}