Skip to content

Commit a41e6c1

Browse files
committed
[WIP] direct_url
1 parent a85e63d commit a41e6c1

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed

src/packaging/direct_url.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
import sys
5+
from collections.abc import Mapping
6+
from dataclasses import dataclass
7+
from pathlib import Path
8+
from typing import TYPE_CHECKING, Any
9+
10+
if TYPE_CHECKING: # pragma: no cover
11+
if sys.version_info >= (3, 11):
12+
from typing import Self
13+
else:
14+
from typing_extensions import Self
15+
16+
17+
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
18+
return {key: value for key, value in data if value is not None}
19+
20+
21+
class DirectUrlValidationError(Exception):
22+
"""Raised when when input data is not spec-compliant."""
23+
24+
context: str | None = None
25+
message: str
26+
27+
def __init__(
28+
self,
29+
cause: str | Exception,
30+
*,
31+
context: str | None = None,
32+
) -> None:
33+
if isinstance(cause, DirectUrlValidationError):
34+
if cause.context:
35+
self.context = (
36+
f"{context}.{cause.context}" if context else cause.context
37+
)
38+
else:
39+
self.context = context
40+
self.message = cause.message
41+
else:
42+
self.context = context
43+
self.message = str(cause)
44+
45+
def __str__(self) -> str:
46+
if self.context:
47+
return f"{self.message} in {self.context!r}"
48+
return self.message
49+
50+
51+
@dataclass(frozen=True, init=False)
52+
class VcsInfo:
53+
vcs: str
54+
requested_revision: str | None = None
55+
commit_id: str
56+
57+
@classmethod
58+
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
59+
60+
61+
@dataclass(frozen=True, init=False)
62+
class ArchiveInfo:
63+
hashes: Mapping[str, str] | None = None
64+
hash: str | None = None # Deprecated, use `hashes` instead
65+
66+
@classmethod
67+
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
68+
...
69+
# XXX validate hashes (see pylock)
70+
# XXX log a warning if `hash` is used (probably not useful by lack of context)?
71+
72+
73+
@dataclass(frozen=True, init=False)
74+
class DirInfo:
75+
editable: bool = False
76+
77+
@classmethod
78+
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
79+
80+
81+
@dataclass(frozen=True, init=False)
82+
class DirectUrl:
83+
url: str
84+
archive_info: ArchiveInfo | None = None
85+
vcs_info: VcsInfo | None = None
86+
dir_info: DirInfo | None = None
87+
subdirectory: Path | None = None
88+
89+
@classmethod
90+
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
91+
...
92+
# XXX exactly one of vcs_info, archive_info, dir_info must be present
93+
# XXX subdirectory must be relative
94+
# XXX if dir_info is present, url scheme must be file://
95+
96+
@classmethod
97+
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
98+
return cls._from_dict(d)
99+
100+
def to_dict(self) -> Mapping[str, Any]:
101+
return dataclasses.asdict(self, dict_factory=_json_dict_factory)
102+
103+
def validate(self) -> None:
104+
"""Validate the DirectUrl instance against the specification.
105+
106+
Raises :class:`DirectUrlValidationError` otherwise.
107+
"""
108+
self.from_dict(self.to_dict())

0 commit comments

Comments
 (0)