{"record":{"id":"0428dc15126c64c3","repo":"nodejs/node","slug":"invalid-wheel-filename-extension-must-be-whl","errorCode":null,"errorMessage":"Invalid wheel filename (extension must be '.whl'): {filename}","messagePattern":"Invalid wheel filename \\(extension must be '\\.whl'\\): (.+?)","errorType":"validation","errorClass":"InvalidWheelFilename","httpStatus":null,"severity":"error","filePath":"tools/gyp/pylib/packaging/utils.py","lineNumber":107,"sourceCode":"    if parsed.post is not None:\n        parts.append(f\".post{parsed.post}\")\n\n    # Development release\n    if parsed.dev is not None:\n        parts.append(f\".dev{parsed.dev}\")\n\n    # Local version segment\n    if parsed.local is not None:\n        parts.append(f\"+{parsed.local}\")\n\n    return \"\".join(parts)\n\n\ndef parse_wheel_filename(\n    filename: str,\n) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:\n    if not filename.endswith(\".whl\"):\n        raise InvalidWheelFilename(\n            f\"Invalid wheel filename (extension must be '.whl'): {filename}\"\n        )\n\n    filename = filename[:-4]\n    dashes = filename.count(\"-\")\n    if dashes not in (4, 5):\n        raise InvalidWheelFilename(\n            f\"Invalid wheel filename (wrong number of parts): {filename}\"\n        )\n\n    parts = filename.split(\"-\", dashes - 2)\n    name_part = parts[0]\n    # See PEP 427 for the rules on escaping the project name.\n    if \"__\" in name_part or re.match(r\"^[\\w\\d._]*$\", name_part, re.UNICODE) is None:\n        raise InvalidWheelFilename(f\"Invalid project name: {filename}\")\n    name = canonicalize_name(name_part)\n\n    try:","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/nodejs/node/blob/1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e/tools/gyp/pylib/packaging/utils.py#L89-L125","documentation":"packaging.utils.InvalidWheelFilename raised by parse_wheel_filename when the filename does not end with '.whl'. This is the first sanity check before PEP 427 structural parsing; any other extension (or none) is rejected immediately.","triggerScenarios":"Calling parse_wheel_filename('foo-1.0.tar.gz'), parse_wheel_filename('foo-1.0-py3-none-any.zip'), or any string lacking the .whl suffix.","commonSituations":"Routing sdist and wheel filenames through the same parser, or passing a URL/path whose extension was stripped or altered.","solutions":["Branch on extension before parsing: use parse_sdist_filename for .tar.gz/.zip and parse_wheel_filename for .whl.","Verify the filename ends with '.whl' (e.g. Path(filename).suffix == '.whl') before calling.","Catch InvalidWheelFilename and skip or log non-wheel entries when iterating a list of mixed filenames."],"exampleFix":"# before\nname, ver, build, tags = parse_wheel_filename(fname)\n\n# after\nif fname.endswith('.whl'):\n    name, ver, build, tags = parse_wheel_filename(fname)\nelif fname.endswith(('.tar.gz', '.zip')):\n    name, ver = parse_sdist_filename(fname)\nelse:\n    raise ValueError(f'Unknown distribution type: {fname}')","handlingStrategy":"type-guard","validationCode":"def is_wheel(filename: str) -> bool:\n    return filename.endswith('.whl')","typeGuard":"from pathlib import Path\ndef is_wheel_path(p) -> bool:\n    return Path(p).suffix == '.whl'","tryCatchPattern":"try:\n    parsed = parse_wheel_filename(filename)\nexcept InvalidWheelFilename:\n    pass  # route to sdist parser or skip","preventionTips":["Branch on suffix before choosing wheel vs sdist parser.","Filter directory listings by extension before batch parsing.","Use pathlib.Path.suffix for robust extension checks."],"tags":["packaging","utils","wheel","filename","pep427"],"backgroundTag":null,"analyzedSha":"1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e","analyzedAt":"2026-08-13T00:53:24.642Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}