-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better freeze of distributions installed from direct URL references #7612
Changes from all commits
6b7f4ce
6f689f6
88582c2
bd4d52b
f779447
94b7713
a0ed759
196706d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
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: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. try:
value = d[key]
except KeyError:
return default is more Pythonic IMO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh. Is it? I've used both over time and I now try to avoid exceptions for flow control. |
||||||
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] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also find list comprehension more readable, as its more explicit. With filter I have to think for a fraction of a second what |
||||||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this file would likely end in https://github.com/pypa/packaging once the PEP is accepted ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly. I've been careful to put only (and all) the PEP 610 implementation in this file with the corresponding tests, so it can be extracted easily. pip-specific stuff is in direct_url_helpers.py.