-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7612 from sbidoul/pip609-v2-sbi
Better freeze of distributions installed from direct URL references
- Loading branch information
Showing
14 changed files
with
916 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pip now implements PEP 610, so ``pip freeze`` has better fidelity | ||
in presence of distributions installed from Direct URL requirements. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
""" PEP 610 """ | ||
import json | ||
import re | ||
|
||
from pip._vendor import six | ||
from pip._vendor.six.moves.urllib import parse as urllib_parse | ||
|
||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING | ||
|
||
if MYPY_CHECK_RUNNING: | ||
from typing import ( | ||
Any, Dict, Iterable, Optional, Type, TypeVar, Union | ||
) | ||
|
||
T = TypeVar("T") | ||
|
||
|
||
DIRECT_URL_METADATA_NAME = "direct_url.json" | ||
ENV_VAR_RE = re.compile(r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$") | ||
|
||
__all__ = [ | ||
"DirectUrl", | ||
"DirectUrlValidationError", | ||
"DirInfo", | ||
"ArchiveInfo", | ||
"VcsInfo", | ||
] | ||
|
||
|
||
class DirectUrlValidationError(Exception): | ||
pass | ||
|
||
|
||
def _get(d, expected_type, key, default=None): | ||
# type: (Dict[str, Any], Type[T], str, Optional[T]) -> Optional[T] | ||
"""Get value from dictionary and verify expected type.""" | ||
if key not in d: | ||
return default | ||
value = d[key] | ||
if six.PY2 and expected_type is str: | ||
expected_type = six.string_types # type: ignore | ||
if not isinstance(value, expected_type): | ||
raise DirectUrlValidationError( | ||
"{!r} has unexpected type for {} (expected {})".format( | ||
value, key, expected_type | ||
) | ||
) | ||
return value | ||
|
||
|
||
def _get_required(d, expected_type, key, default=None): | ||
# type: (Dict[str, Any], Type[T], str, Optional[T]) -> T | ||
value = _get(d, expected_type, key, default) | ||
if value is None: | ||
raise DirectUrlValidationError("{} must have a value".format(key)) | ||
return value | ||
|
||
|
||
def _exactly_one_of(infos): | ||
# type: (Iterable[Optional[InfoType]]) -> InfoType | ||
infos = [info for info in infos if info is not None] | ||
if not infos: | ||
raise DirectUrlValidationError( | ||
"missing one of archive_info, dir_info, vcs_info" | ||
) | ||
if len(infos) > 1: | ||
raise DirectUrlValidationError( | ||
"more than one of archive_info, dir_info, vcs_info" | ||
) | ||
assert infos[0] is not None | ||
return infos[0] | ||
|
||
|
||
def _filter_none(**kwargs): | ||
# type: (Any) -> Dict[str, Any] | ||
"""Make dict excluding None values.""" | ||
return {k: v for k, v in kwargs.items() if v is not None} | ||
|
||
|
||
class VcsInfo(object): | ||
name = "vcs_info" | ||
|
||
def __init__( | ||
self, | ||
vcs, # type: str | ||
commit_id, # type: str | ||
requested_revision=None, # type: Optional[str] | ||
resolved_revision=None, # type: Optional[str] | ||
resolved_revision_type=None, # type: Optional[str] | ||
): | ||
self.vcs = vcs | ||
self.requested_revision = requested_revision | ||
self.commit_id = commit_id | ||
self.resolved_revision = resolved_revision | ||
self.resolved_revision_type = resolved_revision_type | ||
|
||
@classmethod | ||
def _from_dict(cls, d): | ||
# type: (Optional[Dict[str, Any]]) -> Optional[VcsInfo] | ||
if d is None: | ||
return None | ||
return cls( | ||
vcs=_get_required(d, str, "vcs"), | ||
commit_id=_get_required(d, str, "commit_id"), | ||
requested_revision=_get(d, str, "requested_revision"), | ||
resolved_revision=_get(d, str, "resolved_revision"), | ||
resolved_revision_type=_get(d, str, "resolved_revision_type"), | ||
) | ||
|
||
def _to_dict(self): | ||
# type: () -> Dict[str, Any] | ||
return _filter_none( | ||
vcs=self.vcs, | ||
requested_revision=self.requested_revision, | ||
commit_id=self.commit_id, | ||
resolved_revision=self.resolved_revision, | ||
resolved_revision_type=self.resolved_revision_type, | ||
) | ||
|
||
|
||
class ArchiveInfo(object): | ||
name = "archive_info" | ||
|
||
def __init__( | ||
self, | ||
hash=None, # type: Optional[str] | ||
): | ||
self.hash = hash | ||
|
||
@classmethod | ||
def _from_dict(cls, d): | ||
# type: (Optional[Dict[str, Any]]) -> Optional[ArchiveInfo] | ||
if d is None: | ||
return None | ||
return cls(hash=_get(d, str, "hash")) | ||
|
||
def _to_dict(self): | ||
# type: () -> Dict[str, Any] | ||
return _filter_none(hash=self.hash) | ||
|
||
|
||
class DirInfo(object): | ||
name = "dir_info" | ||
|
||
def __init__( | ||
self, | ||
editable=False, # type: bool | ||
): | ||
self.editable = editable | ||
|
||
@classmethod | ||
def _from_dict(cls, d): | ||
# type: (Optional[Dict[str, Any]]) -> Optional[DirInfo] | ||
if d is None: | ||
return None | ||
return cls( | ||
editable=_get_required(d, bool, "editable", default=False) | ||
) | ||
|
||
def _to_dict(self): | ||
# type: () -> Dict[str, Any] | ||
return _filter_none(editable=self.editable or None) | ||
|
||
|
||
if MYPY_CHECK_RUNNING: | ||
InfoType = Union[ArchiveInfo, DirInfo, VcsInfo] | ||
|
||
|
||
class DirectUrl(object): | ||
|
||
def __init__( | ||
self, | ||
url, # type: str | ||
info, # type: InfoType | ||
subdirectory=None, # type: Optional[str] | ||
): | ||
self.url = url | ||
self.info = info | ||
self.subdirectory = subdirectory | ||
|
||
def _remove_auth_from_netloc(self, netloc): | ||
# type: (str) -> str | ||
if "@" not in netloc: | ||
return netloc | ||
user_pass, netloc_no_user_pass = netloc.split("@", 1) | ||
if ( | ||
isinstance(self.info, VcsInfo) and | ||
self.info.vcs == "git" and | ||
user_pass == "git" | ||
): | ||
return netloc | ||
if ENV_VAR_RE.match(user_pass): | ||
return netloc | ||
return netloc_no_user_pass | ||
|
||
@property | ||
def redacted_url(self): | ||
# type: () -> str | ||
"""url with user:password part removed unless it is formed with | ||
environment variables as specified in PEP 610, or it is ``git`` | ||
in the case of a git URL. | ||
""" | ||
purl = urllib_parse.urlsplit(self.url) | ||
netloc = self._remove_auth_from_netloc(purl.netloc) | ||
surl = urllib_parse.urlunsplit( | ||
(purl.scheme, netloc, purl.path, purl.query, purl.fragment) | ||
) | ||
return surl | ||
|
||
def validate(self): | ||
# type: () -> None | ||
self.from_dict(self.to_dict()) | ||
|
||
@classmethod | ||
def from_dict(cls, d): | ||
# type: (Dict[str, Any]) -> DirectUrl | ||
return DirectUrl( | ||
url=_get_required(d, str, "url"), | ||
subdirectory=_get(d, str, "subdirectory"), | ||
info=_exactly_one_of( | ||
[ | ||
ArchiveInfo._from_dict(_get(d, dict, "archive_info")), | ||
DirInfo._from_dict(_get(d, dict, "dir_info")), | ||
VcsInfo._from_dict(_get(d, dict, "vcs_info")), | ||
] | ||
), | ||
) | ||
|
||
def to_dict(self): | ||
# type: () -> Dict[str, Any] | ||
res = _filter_none( | ||
url=self.redacted_url, | ||
subdirectory=self.subdirectory, | ||
) | ||
res[self.info.name] = self.info._to_dict() | ||
return res | ||
|
||
@classmethod | ||
def from_json(cls, s): | ||
# type: (str) -> DirectUrl | ||
return cls.from_dict(json.loads(s)) | ||
|
||
def to_json(self): | ||
# type: () -> str | ||
return json.dumps(self.to_dict(), sort_keys=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.