-
-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathlink.py
173 lines (142 loc) · 5.41 KB
/
link.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""Remote or local file link."""
from __future__ import annotations
import dataclasses as dc
import os
import pathlib
import sys
from typing import Any, cast
from urllib.parse import ParseResult, unquote, urlparse
from unearth.utils import (
add_ssh_scheme_to_git_uri,
parse_query,
path_to_url,
split_auth_from_url,
url_to_path,
)
if sys.version_info >= (3, 8):
from functools import cached_property
else:
from cached_property import cached_property
VCS_SCHEMA = ("git", "hg", "svn", "bzr")
SUPPORTED_HASHES = ("sha1", "sha224", "sha384", "sha256", "sha512", "md5")
@dc.dataclass
class Link:
"""A link can refer to either a remote url or local file.
Args:
url (str): The url of the remote file.
comes_from (str|None): The index page that contains this link
yank_reason (str|None): The reason why this link is yanked
requires_python (str|None): The data-python-requires attribute of this link
dist_info_metadata (str|None): (PEP 658) The hash name and value of the
dist-info metadata, or true if hash is not available
hashes (dict[str, str]|None): The hash name and value of the link from
JSON simple API
vcs (str|None): The vcs type of this link(git/hg/svn/bzr)
"""
url: str
comes_from: str | None = None
yank_reason: str | None = None
requires_python: str | None = None
dist_info_metadata: bool | dict[str, str] | None = None
hashes: dict[str, str] | None = None
vcs: str | None = dc.field(init=False, default=None)
def __post_init__(self) -> None:
vcs_prefixes = tuple(f"{schema}+" for schema in VCS_SCHEMA)
if self.url.startswith(vcs_prefixes):
self.vcs, _, url = self.url.partition("+")
self.normalized = f"{self.vcs}+{add_ssh_scheme_to_git_uri(url)}"
else:
self.normalized = self.url
def as_json(self) -> dict[str, Any]:
"""Return the JSON representation of link object"""
return {
"url": self.redacted,
"comes_from": self.comes_from,
"yank_reason": self.yank_reason,
"requires_python": self.requires_python,
"metadata": self.dist_info_link.url_without_fragment
if self.dist_info_link
else None,
}
def __ident(self) -> tuple:
return (self.normalized, self.yank_reason, self.requires_python)
@cached_property
def parsed(self) -> ParseResult:
return urlparse(self.normalized)
def __repr__(self) -> str:
return f"<Link {self.redacted} (from {self.comes_from})>"
def __hash__(self) -> int:
return hash(self.__ident())
def __eq__(self, __o: object) -> bool:
return isinstance(__o, Link) and self.__ident() == __o.__ident()
@classmethod
def from_path(cls, file_path: str | pathlib.Path) -> Link:
"""Create a link from a local file path"""
url = path_to_url(str(file_path))
return cls(url)
@property
def is_file(self) -> bool:
return self.parsed.scheme == "file"
@property
def file_path(self) -> pathlib.Path:
return pathlib.Path(url_to_path(self.url_without_fragment))
@property
def is_vcs(self) -> bool:
return self.vcs is not None
@property
def filename(self) -> str:
return os.path.basename(unquote(self.parsed.path))
@property
def dist_info_link(self) -> Link | None:
return (
type(self)(f"{self.url_without_fragment}.metadata", self.comes_from)
if self.dist_info_metadata is not None
else None
)
@property
def is_wheel(self) -> bool:
return self.filename.endswith(".whl")
@cached_property
def url_without_fragment(self) -> str:
"""Return the url without the fragment."""
return self.parsed._replace(fragment="").geturl()
@property
def subdirectory(self) -> str | None:
return self._fragment_dict.get("subdirectory")
@property
def _fragment_dict(self) -> dict[str, str]:
return parse_query(self.parsed.fragment)
@property
def redacted(self) -> str:
_, has_auth, host = self.parsed.netloc.rpartition("@")
if not has_auth:
return self.url_without_fragment
netloc = f"***{has_auth}{host}"
return self.parsed._replace(netloc=netloc, fragment="").geturl()
def split_auth(self) -> tuple[tuple[str, str | None] | None, str]:
"""Split the url into ((user, password), host)"""
return split_auth_from_url(self.normalized)
@property
def hash_name(self) -> str | None:
"""Return the hash name of the link if a hash is present."""
result = next(
(name for name in SUPPORTED_HASHES if name in self._fragment_dict), None
)
return result
@property
def hash(self) -> str | None:
"""The hash value associated with the URL"""
if not self.hash_name:
return None
return self._fragment_dict.get(self.hash_name)
@property
def is_yanked(self) -> bool:
return self.yank_reason is not None
@property
def hash_option(self) -> dict[str, list[str]] | None:
"""Return the hash option for the downloader to use"""
if self.hashes:
return {name: [value] for name, value in self.hashes.items()}
if self.hash_name:
return {self.hash_name: [cast(str, self.hash)]}
return None