{"id":"0fef78e18fd342ec","repo":"pypa/pip","slug":"exactly-one-of-vcs-directory-archive-must-be-set","errorCode":null,"errorMessage":"Exactly one of vcs, directory, archive must be set if sdist and wheels are not set","messagePattern":"Exactly one of vcs, directory, archive must be set if sdist and wheels are not set","errorType":"validation","errorClass":"PylockValidationError","httpStatus":null,"severity":"error","filePath":"src/pip/_vendor/packaging/pylock.py","lineNumber":593,"sourceCode":"            vcs=_get_object(d, PackageVcs, \"vcs\"),\n            directory=_get_object(d, PackageDirectory, \"directory\"),\n            archive=_get_object(d, PackageArchive, \"archive\"),\n            index=_get(d, str, \"index\"),\n            sdist=_get_object(d, PackageSdist, \"sdist\"),\n            wheels=_get_sequence_of_objects(d, PackageWheel, \"wheels\"),\n            attestation_identities=_get_sequence(d, Mapping, \"attestation-identities\"),  # type: ignore[type-abstract]\n            tool=_get(d, Mapping, \"tool\"),  # type: ignore[type-abstract]\n        )\n        distributions = bool(package.sdist) + len(package.wheels or [])\n        direct_urls = (\n            bool(package.vcs) + bool(package.directory) + bool(package.archive)\n        )\n        if distributions > 0 and direct_urls > 0:\n            raise PylockValidationError(\n                \"None of vcs, directory, archive must be set if sdist or wheels are set\"\n            )\n        if distributions == 0 and direct_urls != 1:\n            raise PylockValidationError(\n                \"Exactly one of vcs, directory, archive must be set \"\n                \"if sdist and wheels are not set\"\n            )\n        for i, wheel in enumerate(package.wheels or []):\n            try:\n                (name, version, _, _) = parse_wheel_filename(wheel.filename)\n            except Exception as e:\n                raise PylockValidationError(\n                    f\"Invalid wheel filename {wheel.filename!r}\",\n                    context=f\"wheels[{i}]\",\n                ) from e\n            if name != package.name:\n                raise PylockValidationError(\n                    f\"Name in {wheel.filename!r} is not consistent with \"\n                    f\"package name {package.name!r}\",\n                    context=f\"wheels[{i}]\",\n                )\n            if package.version and version != package.version:","sourceCodeStart":575,"sourceCodeEnd":611,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_vendor/packaging/pylock.py#L575-L611","documentation":"Raised as PylockValidationError by Package._from_dict in packaging.pylock when a package entry has no built distributions (no sdist and no wheels) AND does not have exactly one direct source locator. Without artifacts, exactly one of vcs/directory/archive must pin the source so resolution is unambiguous.","triggerScenarios":"A [[packages]] table with neither sdist nor wheels and zero source locators (nothing to install), or with two/three source locators (e.g. both vcs and directory). The check: distributions == 0 and direct_urls != 1.","commonSituations":"Hand-editing a lockfile and deleting the artifact block without adding a source; specifying both a Git URL and a local directory for the same package; a resolver that emits a package with no resolvable source.","solutions":["If pinning to source, provide exactly one of vcs, directory, or archive.","If pinning to artifacts, ensure sdist or wheels is populated (which makes this branch not fire).","Regenerate the lockfile with a resolver that always records one resolution.","Remove the conflicting extra locator so only one remains."],"exampleFix":"# before\n[packages]\nname = \\\"foo\\\"\nvcs = { url = \\\"https://github.com/x/foo\\\" }\ndirectory = \\\"./foo\\\"\n# after\n[packages]\nname = \\\"foo\\\"\ndirectory = \\\"./foo\\\"","handlingStrategy":"validation","validationCode":"def has_exactly_one_source(pkg: dict) -> bool:\n    has_dist = bool(pkg.get('sdist')) or bool(pkg.get('wheels'))\n    srcs = bool(pkg.get('vcs')) + bool(pkg.get('directory')) + bool(pkg.get('archive'))\n    return has_dist or srcs == 1","typeGuard":"def is_valid_source_count(pkg: dict) -> bool:\n    dists = bool(pkg.get('sdist')) + len(pkg.get('wheels') or [])\n    srcs = bool(pkg.get('vcs')) + bool(pkg.get('directory')) + bool(pkg.get('archive'))\n    return dists > 0 or srcs == 1","tryCatchPattern":"try:\n    PylockFile.from_dict(data)\nexcept PylockValidationError as e:\n    if 'Exactly one of vcs, directory, archive' in str(e):\n        keep_single_source_locator(e.context)","preventionTips":["When pinning to source, set exactly one of vcs/directory/archive.","When pinning to artifacts, populate sdist or wheels.","Validate the count of source locators before loading.","Use a resolver that always records one resolution per package."],"tags":["pylock","pep771","validation","lockfile-structure"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}