pypa/pip · error · DirectUrlValidationError

Exactly one of vcs_info, archive_info, dir_info must be…

Error message

Exactly one of vcs_info, archive_info, dir_info must be present

What it means

Per PEP 610, a direct_url.json must describe exactly one source type: a VCS checkout (vcs_info), an archive download (archive_info), or a local directory (dir_info). DirectUrl._from_dict validates that exactly one of these three fields is present (not zero, not two or more). If the count is not exactly 1, DirectUrlValidationError is raised.

Solutions

  1. Include exactly one info block based on the actual source type of the package
  2. Remove extra info blocks if multiple are present
  3. Add the appropriate info block if none is present (use empty dict {} for minimal archive_info)

Example fix

# before (no info block)
data = {"url": "https://github.com/org/repo.git"}
# or (multiple info blocks)
data = {"url": "...", "archive_info": {}, "vcs_info": {"vcs": "git", "commit_id": "abc"}}

# after (exactly one)
data = {"url": "https://example.com/pkg.tar.gz", "archive_info": {"hashes": {"sha256": "..."}}}
Defensive patterns

Strategy: validation

Validate before calling

def validate_single_info_block(data: dict) -> None:
    info_keys = [k for k in ("vcs_info", "archive_info", "dir_info") if k in data and data[k] is not None]
    if len(info_keys) != 1:
        raise ValueError(f"Exactly one of vcs_info, archive_info, dir_info required, found: {info_keys}")

Type guard

def has_exactly_one_info_block(data: dict) -> bool:
    present = [k for k in ("vcs_info", "archive_info", "dir_info") if data.get(k) is not None]
    return len(present) == 1

Try / catch

from packaging.direct_url import DirectUrl, DirectUrlValidationError

try:
    du = DirectUrl.from_dict(data)
except DirectUrlValidationError as e:
    if "Exactly one" in str(e):
        # determine correct info type from URL scheme and add it
        pass

Prevention

When it happens

Trigger: Calling DirectUrl.from_dict(d) where none of the three info blocks are present (just a bare URL), or where two or more are present simultaneously (e.g. both vcs_info and archive_info).

Common situations: Hand-crafted or programmatically generated metadata missing the required info block. Metadata from buggy tools that include multiple info types. Schema drift after metadata format changes.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/d775e8e0a8a454eb. Report an issue: GitHub.

Appendix: source

Thrown at src/pip/_vendor/packaging/direct_url.py:294

        object.__setattr__(self, "vcs_info", vcs_info)
        object.__setattr__(self, "dir_info", dir_info)
        object.__setattr__(self, "subdirectory", subdirectory)

    @classmethod
    def _from_dict(cls, d: Mapping[str, Any]) -> Self:
        direct_url = cls(
            url=_get_required(d, str, "url"),
            archive_info=_get_object(d, ArchiveInfo, "archive_info"),
            vcs_info=_get_object(d, VcsInfo, "vcs_info"),
            dir_info=_get_object(d, DirInfo, "dir_info"),
            subdirectory=_get(d, str, "subdirectory"),
        )
        if (
            bool(direct_url.vcs_info)
            + bool(direct_url.archive_info)
            + bool(direct_url.dir_info)
        ) != 1:
            raise DirectUrlValidationError(
                "Exactly one of vcs_info, archive_info, dir_info must be present"
            )
        if direct_url.dir_info is not None:
            parsed_url = urllib.parse.urlsplit(direct_url.url)
            if parsed_url.scheme != "file":
                raise DirectUrlValidationError(
                    "URL scheme must be file:// when dir_info is present",
                    context="url",
                )
            if not _file_url_has_absolute_path(parsed_url):
                raise DirectUrlValidationError(
                    "File URL must be absolute when dir_info is present",
                    context="url",
                )
        # XXX subdirectory must be relative, can we, should we validate that here?
        return direct_url

    @classmethod

View on GitHub (pinned to f399c37189)