From eb32fc15c7bb7bd9a3777df1ecbda21853c45c49 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 23 Jan 2019 16:57:28 -0500 Subject: [PATCH 01/35] Add support for PEP-508 direct-URL dependencies - Fixes #108 Signed-off-by: Dan Ryan --- news/108.feature.rst | 1 + src/requirementslib/models/requirements.py | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 news/108.feature.rst diff --git a/news/108.feature.rst b/news/108.feature.rst new file mode 100644 index 00000000..0e8df2cf --- /dev/null +++ b/news/108.feature.rst @@ -0,0 +1 @@ +Provide support for parsing PEP-508 compliant direct URL dependencies. diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 81ff2b95..2847b522 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -1784,6 +1784,9 @@ def from_line(cls, line): line_is_vcs = is_vcs(line) # check for pep-508 compatible requirements name, _, possible_url = line.partition("@") + name = name.strip() + if possible_url is not None: + possible_url = possible_url.strip() r = None # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] if is_installable_file(line) or ( (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and From 3cdecfcd5091a4c51169ec4ead437fe85136c427 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 23 Jan 2019 19:46:51 -0500 Subject: [PATCH 02/35] Fix pip monkeypatch and ireq generation Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 32 +++++++++++++--------- src/requirementslib/models/vcs.py | 5 +++- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 2847b522..3be66d5c 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -104,6 +104,7 @@ def __init__(self, line): self.parsed_marker = None # type: Optional[Marker] self.preferred_scheme = None # type: Optional[str] self.requirement = None # type: Optional[PackagingRequirement] + self.is_direct_url = False # type: bool self._parsed_url = None # type: Optional[urllib_parse.ParseResult] self._setup_cfg = None # type: Optional[str] self._setup_py = None # type: Optional[str] @@ -253,6 +254,8 @@ def get_url(self): name, _, url = self.line.partition("@") if self.name is None: self.name = name + if is_valid_url(url): + self.is_direct_url = True line = url.strip() parsed = urllib_parse.urlparse(line) self._parsed_url = parsed @@ -395,17 +398,15 @@ def get_ireq(self): elif (self.is_file or self.is_url) and not self.is_vcs: line = self.line scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" - if self.setup_py: - line = os.path.dirname(os.path.abspath(self.setup_py)) - elif self.setup_cfg: - line = os.path.dirname(os.path.abspath(self.setup_cfg)) - elif self.pyproject_toml: - line = os.path.dirname(os.path.abspath(self.pyproject_toml)) + local_line = next(iter([ + os.path.dirname(os.path.abspath(f)) for f in [ + self.setup_py, self.setup_cfg, self.pyproject_toml + ] if f is not None + ]), None) + line = local_line if local_line is not None else self.line if scheme == "path": if not line and self.base_path is not None: line = os.path.abspath(self.base_path) - # if self.extras: - # line = pip_shims.shims.path_to_url(line) else: if self.link is not None: line = self.link.url_without_fragment @@ -414,8 +415,6 @@ def get_ireq(self): line = self.uri else: line = self.path - if self.extras: - line = "{0}[{1}]".format(line, ",".join(sorted(set(self.extras)))) if self.editable: ireq = pip_shims.shims.install_req_from_editable(self.link.url) else: @@ -435,9 +434,9 @@ def parse_ireq(self): # type: () -> None if self._ireq is None: self._ireq = self.get_ireq() - # if self._ireq is not None: - # if self.requirement is not None and self._ireq.req is None: - # self._ireq.req = self.requirement + if self._ireq is not None: + if self.requirement is not None and self._ireq.req is None: + self._ireq.req = self.requirement def _parse_wheel(self): # type: () -> Optional[str] @@ -1782,11 +1781,15 @@ def from_line(cls, line): # Installable local files and installable non-vcs urls are handled # as files, generally speaking line_is_vcs = is_vcs(line) + is_direct_url = False # check for pep-508 compatible requirements name, _, possible_url = line.partition("@") name = name.strip() if possible_url is not None: possible_url = possible_url.strip() + is_direct_url = is_valid_url(possible_url) + if not is_direct_url and not line_is_vcs: + line_is_vcs = is_vcs(possible_url) r = None # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] if is_installable_file(line) or ( (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and @@ -1849,6 +1852,9 @@ def from_line(cls, line): if hashes: args["hashes"] = hashes # type: ignore cls_inst = cls(**args) + if is_direct_url: + setup_info = cls_inst.run_requires() + cls_inst.specifiers = "=={0}".format(setup_info.get("version")) return cls_inst @classmethod diff --git a/src/requirementslib/models/vcs.py b/src/requirementslib/models/vcs.py index e53927e7..aa9e051e 100644 --- a/src/requirementslib/models/vcs.py +++ b/src/requirementslib/models/vcs.py @@ -83,7 +83,10 @@ def monkeypatch_pip(cls): new_defaults = [False,] + list(run_command_defaults)[1:] new_defaults = tuple(new_defaults) if six.PY3: - pip_vcs.VersionControl.run_command.__defaults__ = new_defaults + try: + pip_vcs.VersionControl.run_command.__defaults__ = new_defaults + except AttributeError: + pip_vcs.VersionControl.run_command.__func__.__defaults__ = new_defaults else: pip_vcs.VersionControl.run_command.__func__.__defaults__ = new_defaults sys.modules[target_module] = pip_vcs From 081a19ff0c46351ced8fdf957af70bf62afaaf07 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 23 Jan 2019 20:55:23 -0500 Subject: [PATCH 03/35] Add tests Signed-off-by: Dan Ryan --- tests/unit/test_requirements.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py index ad0fa125..213f3d10 100644 --- a/tests/unit/test_requirements.py +++ b/tests/unit/test_requirements.py @@ -299,3 +299,14 @@ def test_local_editable_ref(): path = Path(ARTIFACTS_DIR) / 'git/requests' req = Requirement.from_pipfile("requests", {"editable": True, "git": path.as_uri(), "ref": "2.18.4"}) assert req.as_line() == "-e git+{0}@2.18.4#egg=requests".format(path.as_uri()) + + +def test_pep_508(): + r = Requirement.from_line("tablib@ https://github.com/kennethreitz/tablib/archive/v0.12.1.zip") + assert r.specifiers == "==0.12.1" + assert r.req.link.url == "https://github.com/kennethreitz/tablib/archive/v0.12.1.zip" + assert r.req.req.name == "tablib" + assert r.req.req.url == "https://github.com/kennethreitz/tablib/archive/v0.12.1.zip" + requires, setup_requires, build_requires = r.req.dependencies + assert all(dep in requires for dep in ["openpyxl", "odfpy", "xlrd"]) + assert r.as_pipfile() == {'tablib': {'file': 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip'}} From 9697f0bef8277fe30c8ead31db269c98747eb308 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Thu, 24 Jan 2019 01:51:44 -0500 Subject: [PATCH 04/35] Full implementation of pep 508 style requirements Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 85 +++++++++++++++++----- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 3be66d5c..4403950a 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -140,9 +140,12 @@ def split_hashes(cls, line): @property def line_with_prefix(self): # type: () -> str + line = self.line + if self.is_direct_url: + line = self.link.url if self.editable: - return "-e {0}".format(self.line) - return self.line + return "-e {0}".format(line) + return line @property def base_path(self): @@ -236,7 +239,22 @@ def parse_extras(self): :rtype: None """ - self.line, extras = pip_shims.shims._strip_extras(self.line) + extras = None + if "@" in self.line: + parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(self.line)) + if not parsed.scheme: + name, _, line = self.line.partition("@") + name = name.strip() + line = line.strip() + if is_vcs(line) or is_valid_url(line): + self.is_direct_url = True + name, extras = pip_shims.shims._strip_extras(name) + self.name = name + self.line = line + else: + self.line, extras = pip_shims.shims._strip_extras(self.line) + else: + self.line, extras = pip_shims.shims._strip_extras(self.line) if extras is not None: self.extras = parse_extras(extras) @@ -397,6 +415,8 @@ def get_ireq(self): ireq = pip_shims.shims.install_req_from_line(self.line) elif (self.is_file or self.is_url) and not self.is_vcs: line = self.line + if self.is_direct_url: + line = self.link.url scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" local_line = next(iter([ os.path.dirname(os.path.abspath(f)) for f in [ @@ -498,6 +518,8 @@ def _parse_requirement_from_vcs(self): # type: () -> Optional[PackagingRequirement] name = self.name if self.name else self.link.egg_fragment url = self.uri if self.uri else unquote(self.link.url) + if self.is_direct_url: + url = self.link.url if not name: raise ValueError( "pipenv requires an #egg fragment for version controlled " @@ -572,7 +594,12 @@ def parse_link(self): self.relpath = relpath self.path = path self.uri = uri - self._link = link + if self.is_direct_url and self.name is not None: + self._link = create_link( + build_vcs_uri(vcs=vcs, uri=uri, ref=ref, extras=self.extras, name=self.name) + ) + else: + self._link = link def parse_markers(self): # type: () -> None @@ -595,6 +622,7 @@ def parse(self): self.parse_requirement() self.parse_ireq() + @attr.s(slots=True) class NamedRequirement(object): name = attr.ib() # type: str @@ -1308,19 +1336,20 @@ def pipfile_part(self): @attr.s(slots=True) class VCSRequirement(FileRequirement): #: Whether the repository is editable - editable = attr.ib(default=None) + editable = attr.ib(default=None) # type: Optional[bool] #: URI for the repository - uri = attr.ib(default=None) + uri = attr.ib(default=None) # type: Optional[str] #: path to the repository, if it's local - path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) + path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) # type: Optional[str] #: vcs type, i.e. git/hg/svn - vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) + vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) # type: Optional[str] #: vcs reference name (branch / commit / tag) - ref = attr.ib(default=None) + ref = attr.ib(default=None) # type: Optional[str] #: Subdirectory to use for installation if applicable - subdirectory = attr.ib(default=None) - _repo = attr.ib(default=None) - _base_line = attr.ib(default=None) + subdirectory = attr.ib(default=None) # type: Optional[str] + _repo = attr.ib(default=None) # type: Optional['VCSRepository'] + _base_line = attr.ib(default=None) # type: Optional[str] + _parsed_line = attr.ib(default=None) # type: Optional[Line] name = attr.ib() link = attr.ib() req = attr.ib() @@ -1555,16 +1584,32 @@ def from_pipfile(cls, name, pipfile): @classmethod def from_line(cls, line, editable=None, extras=None): relpath = None + parsed_line = Line(line) + if editable: + parsed_line.editable = editable + if extras: + parsed_line.extras = extras if line.startswith("-e "): editable = True line = line.split(" ", 1)[1] + if "@" in line: + parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(line)) + if not parsed.scheme: + possible_name, _, line = line.partition("@") + possible_name = possible_name.strip() + line = line.strip() + possible_name, extras = pip_shims.shims._strip_extras(possible_name) + name = possible_name + line = "{0}#egg={1}".format(line, name) vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line) if not extras and link.egg_fragment: name, extras = pip_shims.shims._strip_extras(link.egg_fragment) - if extras: - extras = parse_extras(extras) else: - name = link.egg_fragment + name, _ = pip_shims.shims._strip_extras(link.egg_fragment) + if extras: + extras = parse_extras(extras) + else: + line, extras = pip_shims.shims._strip_extras(line) subdirectory = link.subdirectory_fragment ref = None if "@" in link.path and "@" in uri: @@ -1575,8 +1620,9 @@ def from_line(cls, line, editable=None, extras=None): ref = _ref if relpath and "@" in relpath: relpath, ref = relpath.rsplit("@", 1) + creation_args = { - "name": name, + "name": name if name else parsed_line.name, "path": relpath or path, "editable": editable, "extras": extras, @@ -1584,7 +1630,8 @@ def from_line(cls, line, editable=None, extras=None): "vcs_type": vcs_type, "line": line, "uri": uri, - "uri_scheme": prefer + "uri_scheme": prefer, + "parsed_line": parsed_line } if relpath: creation_args["relpath"] = relpath @@ -1615,6 +1662,8 @@ def line_part(self): else "{0}" ) base = final_format.format(self.vcs_uri) + elif self._parsed_line is not None and self._parsed_line.is_direct_url: + return self._parsed_line.line_with_prefix elif getattr(self, "_base_line", None): base = self._base_line else: @@ -1788,7 +1837,7 @@ def from_line(cls, line): if possible_url is not None: possible_url = possible_url.strip() is_direct_url = is_valid_url(possible_url) - if not is_direct_url and not line_is_vcs: + if not line_is_vcs: line_is_vcs = is_vcs(possible_url) r = None # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] if is_installable_file(line) or ( From f0549ba0ef6600512822b2f0c3b486d7635caa24 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 29 Jan 2019 01:30:00 -0500 Subject: [PATCH 05/35] Make things hashable Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 172 ++++++++++++++------- src/requirementslib/models/setup_info.py | 170 ++++++++++++++------ src/requirementslib/models/vcs.py | 2 +- tests/unit/test_requirements.py | 66 ++++---- tests/unit/test_utils.py | 12 +- 5 files changed, 282 insertions(+), 140 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 4403950a..75f620e7 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -113,6 +113,7 @@ def __init__(self, line): self._pyproject_backend = None # type: Optional[str] self._wheel_kwargs = None # type: Dict[str, str] self._vcsrepo = None # type: Optional[VCSRepository] + self._setup_info = None # type: Optional[SetupInfo] self._ref = None # type: Optional[str] self._ireq = None # type: Optional[InstallRequirement] self._src_root = None # type: Optional[str] @@ -120,6 +121,12 @@ def __init__(self, line): super(Line, self).__init__() self.parse() + def __hash__(self): + return hash(( + self.editable, self.line, self.markers, tuple(self.extras), + tuple(self.hashes), self.vcs, self.ireq) + ) + @classmethod def split_hashes(cls, line): # type: (str) -> Tuple[str, List[str]] @@ -200,6 +207,7 @@ def populate_setup_paths(self): @property def pyproject_requires(self): + # type: () -> Optional[List[str]] if self._pyproject_requires is None and self.pyproject_toml is not None: pyproject_requires, pyproject_backend = get_pyproject(self.path) self._pyproject_requires = pyproject_requires @@ -208,6 +216,7 @@ def pyproject_requires(self): @property def pyproject_backend(self): + # type: () -> Optional[str] if self._pyproject_requires is None and self.pyproject_toml is not None: pyproject_requires, pyproject_backend = get_pyproject(self.path) if not pyproject_backend and self.setup_cfg is not None: @@ -384,6 +393,13 @@ def is_installable(self): return True return False + @property + def setup_info(self): + # type: () -> Optional[SetupInfo] + if self._setup_info is None: + self._setup_info = SetupInfo.from_ireq(self.ireq) + return self._setup_info + def _get_vcsrepo(self): # type: () -> Optional[VCSRepository] from .vcs import VCSRepository @@ -594,7 +610,7 @@ def parse_link(self): self.relpath = relpath self.path = path self.uri = uri - if self.is_direct_url and self.name is not None: + if self.is_direct_url and self.name is not None and vcs is not None: self._link = create_link( build_vcs_uri(vcs=vcs, uri=uri, ref=ref, extras=self.extras, name=self.name) ) @@ -623,12 +639,12 @@ def parse(self): self.parse_ireq() -@attr.s(slots=True) +@attr.s(slots=True, hash=True) class NamedRequirement(object): name = attr.ib() # type: str version = attr.ib(validator=attr.validators.optional(validate_specifiers)) # type: Optional[str] req = attr.ib() # type: PackagingRequirement - extras = attr.ib(default=attr.Factory(list)) # type: List[str] + extras = attr.ib(default=attr.Factory(list)) # type: Tuple[str] editable = attr.ib(default=False) # type: bool @req.default @@ -654,7 +670,7 @@ def from_line(cls, line): if not name: name = getattr(req, "key", line) req.name = name - extras = None # type: Optional[List[str]] + extras = None # type: Optional[Tuple[str]] if req.extras: extras = list(req.extras) return cls(name=name, version=specifiers, req=req, extras=extras) @@ -700,37 +716,38 @@ def pipfile_part(self): ) -@attr.s(slots=True) +@attr.s(slots=True, hash=True) class FileRequirement(object): """File requirements for tar.gz installable files or wheels or setup.py containing directories.""" #: Path to the relevant `setup.py` location - setup_path = attr.ib(default=None) # type: Optional[str] + setup_path = attr.ib(default=None, cmp=True) # type: Optional[str] #: path to hit - without any of the VCS prefixes (like git+ / http+ / etc) - path = attr.ib(default=None) # type: Optional[str] + path = attr.ib(default=None, cmp=True) # type: Optional[str] #: Whether the package is editable - editable = attr.ib(default=False) # type: bool + editable = attr.ib(default=False, cmp=True) # type: bool #: Extras if applicable - extras = attr.ib(default=attr.Factory(list)) # type: List[str] - _uri_scheme = attr.ib(default=None) # type: Optional[str] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[str] + _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[str] #: URI of the package - uri = attr.ib() # type: Optional[str] + uri = attr.ib(cmp=True) # type: Optional[str] #: Link object representing the package to clone - link = attr.ib() # type: Optional[Link] + link = attr.ib(cmp=True) # type: Optional[Link] #: PyProject Requirements - pyproject_requires = attr.ib(default=attr.Factory(list)) # type: List + pyproject_requires = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple #: PyProject Build System - pyproject_backend = attr.ib(default=None) # type: Optional[str] + pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[str] #: PyProject Path - pyproject_path = attr.ib(default=None) # type: Optional[str] + pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[str] #: Setup metadata e.g. dependencies - setup_info = attr.ib(default=None) # type: SetupInfo - _has_hashed_name = attr.ib(default=False) # type: bool + setup_info = attr.ib(default=None, cmp=True, hash=True) # type: Optional[SetupInfo] + _has_hashed_name = attr.ib(default=False, cmp=True) # type: bool + _parsed_line = attr.ib(default=None, hash=True) # type: Optional[Line] #: Package name - name = attr.ib() # type: Optional[str] + name = attr.ib(cmp=True) # type: Optional[str] #: A :class:`~pkg_resources.Requirement` isntance - req = attr.ib() # type: Optional[PackagingRequirement] + req = attr.ib(cmp=True) # type: Optional[PackagingRequirement] @classmethod def get_link_from_line(cls, line): @@ -853,7 +870,7 @@ def dependencies(self): setup_deps.extend(setup_info.get("setup_requires", [])) build_deps.extend(setup_info.get("build_requires", [])) if self.pyproject_requires: - build_deps.extend(self.pyproject_requires) + build_deps.extend(list(self.pyproject_requires)) setup_deps = list(set(setup_deps)) build_deps = list(set(build_deps)) return deps, setup_deps, build_deps @@ -911,6 +928,9 @@ def get_name(self): setupinfo = None if self.setup_info is not None: setupinfo = self.setup_info + elif self._parsed_line is not None and self._parsed_line.setup_info is not None: + setupinfo = self._parsed_line.setup_info + self._setup_info = self._parsed_line.setup_info else: setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir) if setupinfo: @@ -923,7 +943,7 @@ def get_name(self): build_requires = setupinfo_dict.get("build_requires") build_backend = setupinfo_dict.get("build_backend") if build_requires and not self.pyproject_requires: - self.pyproject_requires = build_requires + self.pyproject_requires = tuple(build_requires) if build_backend and not self.pyproject_backend: self.pyproject_backend = build_backend if not name or name.lower() == "unknown": @@ -949,7 +969,12 @@ def get_link(self): def get_requirement(self): # type: () -> PackagingRequirement if self.name is None: - raise ValueError("Failed to generate a requirement: missing name for {0!r}".format(self)) + if self._parsed_line is not None and self._parsed_line.name is not None: + self.name = self._parsed_line.name + else: + raise ValueError( + "Failed to generate a requirement: missing name for {0!r}".format(self) + ) req = init_requirement(normalize_name(self.name)) req.editable = False if self.link is not None: @@ -1006,6 +1031,8 @@ def is_remote_artifact(self): @property def is_direct_url(self): # type: () -> bool + if self._parsed_line is not None and self._parsed_line.is_direct_url: + return True return self.is_remote_artifact @property @@ -1024,7 +1051,7 @@ def create( path=None, # type: Optional[str] uri=None, # type: str editable=False, # type: bool - extras=None, # type: Optional[List[str]] + extras=None, # type: Optional[Tuple[str]] link=None, # type: Link vcs_type=None, # type: Optional[Any] name=None, # type: Optional[str] @@ -1057,14 +1084,15 @@ def create( if not uri: uri = unquote(link.url_without_fragment) if not extras: - extras = [] + extras = () pyproject_path = None + pyproject_requires = None + pyproject_backend = None if path is not None: pyproject_requires = get_pyproject(path) - pyproject_backend = None - pyproject_requires = None if pyproject_requires is not None: pyproject_requires, pyproject_backend = pyproject_requires + pyproject_requires = tuple(pyproject_requires) if path: setup_paths = get_setup_paths(path) if setup_paths["pyproject_toml"] is not None: @@ -1084,6 +1112,7 @@ def create( "pyproject_requires": pyproject_requires, "pyproject_backend": pyproject_backend, "path": path or relpath, + "parsed_line": parsed_line } if vcs_type: creation_kwargs["vcs"] = vcs_type @@ -1130,10 +1159,10 @@ def create( setup_name = setupinfo_dict.get("name", None) if setup_name: name = setup_name - build_requires = setupinfo_dict.get("build_requires", []) - build_backend = setupinfo_dict.get("build_backend", []) + build_requires = setupinfo_dict.get("build_requires", ()) + build_backend = setupinfo_dict.get("build_backend", ()) if not creation_kwargs.get("pyproject_requires") and build_requires: - creation_kwargs["pyproject_requires"] = build_requires + creation_kwargs["pyproject_requires"] = tuple(build_requires) if not creation_kwargs.get("pyproject_backend") and build_backend: creation_kwargs["pyproject_backend"] = build_backend creation_kwargs["setup_info"] = setup_info @@ -1146,17 +1175,23 @@ def create( creation_req_line = getattr(creation_req, "line", None) if creation_req_line is None and line is not None: creation_kwargs["req"].line = line # type: ignore - if parsed_line.name: + if parsed_line and parsed_line.name: if name and len(parsed_line.name) != 7 and len(name) == 7: name = parsed_line.name + if parsed_line and parsed_line.setup_info is not None: + creation_kwargs["setup_info"] = parsed_line.setup_info if name: creation_kwargs["name"] = name cls_inst = cls(**creation_kwargs) # type: ignore + if parsed_line and not cls_inst._parsed_line: + cls_inst._parsed_line = parsed_line + if not cls_inst._parsed_line: + cls_inst._parsed_line = Line(cls_inst.line_part) return cls_inst @classmethod def from_line(cls, line, extras=None): - # type: (str, Optional[List[str]]) -> FileRequirement + # type: (str, Optional[Tuple[str]]) -> FileRequirement line = line.strip('"').strip("'") link = None path = None @@ -1166,7 +1201,7 @@ def from_line(cls, line, extras=None): name = None req = None if not extras: - extras = [] + extras = () if not any([is_installable_file(line), is_valid_url(line), is_file_url(line)]): try: req = init_requirement(line) @@ -1246,7 +1281,7 @@ def from_pipfile(cls, name, pipfile): "extras": pipfile.get("extras", None) } - extras = pipfile.get("extras", []) + extras = pipfile.get("extras", ()) line = "" if name: if extras: @@ -1258,6 +1293,7 @@ def from_pipfile(cls, name, pipfile): line = link.url if pipfile.get("editable", False): line = "-e {0}".format(line) + arg_dict["line"] = line return cls.create(**arg_dict) @property @@ -1333,7 +1369,7 @@ def pipfile_part(self): return {name: pipfile_dict} -@attr.s(slots=True) +@attr.s(slots=True, hash=True) class VCSRequirement(FileRequirement): #: Whether the repository is editable editable = attr.ib(default=None) # type: Optional[bool] @@ -1349,7 +1385,6 @@ class VCSRequirement(FileRequirement): subdirectory = attr.ib(default=None) # type: Optional[str] _repo = attr.ib(default=None) # type: Optional['VCSRepository'] _base_line = attr.ib(default=None) # type: Optional[str] - _parsed_line = attr.ib(default=None) # type: Optional[Line] name = attr.ib() link = attr.ib() req = attr.ib() @@ -1496,7 +1531,7 @@ def get_vcs_repo(self, src_dir=None): pyproject_info = get_pyproject(checkout_dir) if pyproject_info is not None: pyproject_requires, pyproject_backend = pyproject_info - self.pyproject_requires = pyproject_requires + self.pyproject_requires = tuple(pyproject_requires) self.pyproject_backend = pyproject_backend return vcsrepo @@ -1583,6 +1618,7 @@ def from_pipfile(cls, name, pipfile): @classmethod def from_line(cls, line, editable=None, extras=None): + # type: (str, Optional[bool], Optional[Tuple[str]]) -> VCSRequirement relpath = None parsed_line = Line(line) if editable: @@ -1610,6 +1646,8 @@ def from_line(cls, line, editable=None, extras=None): extras = parse_extras(extras) else: line, extras = pip_shims.shims._strip_extras(line) + if extras: + extras = tuple(extras) subdirectory = link.subdirectory_fragment ref = None if "@" in link.path and "@" in uri: @@ -1705,19 +1743,23 @@ def pipfile_part(self): return {name: pipfile_dict} -@attr.s +@attr.s(cmp=True) class Requirement(object): - name = attr.ib() # type: str - vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs)) # type: Optional[str] - req = attr.ib(default=None) - markers = attr.ib(default=None) - specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers)) + name = attr.ib(cmp=True) # type: str + vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[str] + req = attr.ib(default=None, cmp=True) + markers = attr.ib(default=None, cmp=True) + specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) index = attr.ib(default=None) - editable = attr.ib(default=None) - hashes = attr.ib(default=attr.Factory(list), converter=list) - extras = attr.ib(default=attr.Factory(list)) - abstract_dep = attr.ib(default=None) - _ireq = None + editable = attr.ib(default=None, cmp=True) + hashes = attr.ib(default=attr.Factory(tuple), converter=tuple, cmp=True) # type: Optional[Tuple[str]] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[str]] + abstract_dep = attr.ib(default=None, cmp=False) + line_instance = attr.ib(default=None, cmp=True) # type: Optional[Line] + _ireq = attr.ib(default=None) # type: Optional[pip_shims.InstallRequirement] + + def __hash__(self): + return hash(self.as_line()) @name.default def get_name(self): @@ -1818,12 +1860,13 @@ def from_line(cls, line): if "--hash=" in line: hashes = line.split(" --hash=") line, hashes = hashes[0], hashes[1:] + line_instance = Line(line) editable = line.startswith("-e ") line = line.split(" ", 1)[1] if editable else line line, markers = split_markers_from_line(line) line, extras = pip_shims.shims._strip_extras(line) if extras: - extras = parse_extras(extras) + extras = tuple(parse_extras(extras)) line = line.strip('"').strip("'").strip() line_with_prefix = "-e {0}".format(line) if editable else line vcs = None @@ -1865,7 +1908,7 @@ def from_line(cls, line): if not extras: name, extras = pip_shims.shims._strip_extras(name) if extras: - extras = parse_extras(extras) + extras = tuple(parse_extras(extras)) if version: name = "{0}{1}".format(name, version) r = NamedRequirement.from_line(line) @@ -1888,22 +1931,28 @@ def from_line(cls, line): "req": r, "markers": markers, "editable": editable, + "line_instance": line_instance } if extras: - extras = sorted(dedup([extra.lower() for extra in extras])) + extras = tuple(sorted(dedup([extra.lower() for extra in extras]))) args["extras"] = extras if r is not None: r.extras = extras elif r is not None and r.extras is not None: - args["extras"] = sorted(dedup([extra.lower() for extra in r.extras])) # type: ignore + args["extras"] = tuple(sorted(dedup([extra.lower() for extra in r.extras]))) # type: ignore if r.req is not None: r.req.extras = args["extras"] if hashes: - args["hashes"] = hashes # type: ignore + args["hashes"] = tuple(hashes) # type: ignore cls_inst = cls(**args) - if is_direct_url: - setup_info = cls_inst.run_requires() - cls_inst.specifiers = "=={0}".format(setup_info.get("version")) + if is_direct_url or cls_inst.is_file_or_url or cls_inst.is_vcs: + if not cls_inst.specifiers: + setup_info = cls_inst.run_requires() + cls_inst.specifiers = "=={0}".format(setup_info.get("version")) + if cls_inst.line_instance.ireq: + cls_inst.line_instance._ireq.specifiers = SpecifierSet(cls_inst.specifiers) + if getattr(cls_inst.req, "_parsed_line", None) and cls_inst.req._parsed_line.ireq: + cls_inst.req._parsed_line._ireq.specifiers = SpecifierSet(cls_inst.specifiers) return cls_inst @classmethod @@ -1942,20 +1991,21 @@ def from_pipfile(cls, name, pipfile): extras = _pipfile.get("extras") r.req.specifier = SpecifierSet(_pipfile["version"]) r.req.extras = ( - sorted(dedup([extra.lower() for extra in extras])) if extras else [] + tuple(sorted(dedup([extra.lower() for extra in extras]))) if extras else () ) args = { "name": r.name, "vcs": vcs, "req": r, "markers": markers, - "extras": _pipfile.get("extras"), + "extras": tuple(_pipfile.get("extras", [])), "editable": _pipfile.get("editable", False), "index": _pipfile.get("index"), } if any(key in _pipfile for key in ["hash", "hashes"]): args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")]) cls_inst = cls(**args) + cls_inst.line_instance = Line(cls_inst.as_line()) return cls_inst def as_line( @@ -2085,11 +2135,17 @@ def as_pipfile(self): except AttributeError: hashes.append(_hash) base_dict["hashes"] = sorted(hashes) + if "extras" in base_dict: + base_dict["extras"] = list(base_dict["extras"]) if len(base_dict.keys()) == 1 and "version" in base_dict: base_dict = base_dict.get("version") return {name: base_dict} def as_ireq(self): + if self.line_instance and self.line_instance.ireq: + return self.line_instance.ireq + elif getattr(self.req, "_parsed_line", None) and self.req._parsed_line.ireq: + return self.req._parsed_line.ireq kwargs = { "include_hashes": False, } @@ -2189,6 +2245,8 @@ def find_all_matches(self, sources=None, finder=None): def run_requires(self, sources=None, finder=None): if self.req and self.req.setup_info is not None: info_dict = self.req.setup_info.as_dict() + elif self.line_instance and self.line_instance.setup_info is not None: + info_dict = self.line_instance.setup_info.as_dict() else: from .setup_info import SetupInfo if not finder: diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 13147c4f..6aec5bdb 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -75,12 +75,14 @@ def _get_src_dir(root): if src: return src virtual_env = os.environ.get("VIRTUAL_ENV") - if virtual_env: + if virtual_env is not None: return os.path.join(virtual_env, "src") if not root: # Intentionally don't match pip's behavior here -- this is a temporary copy - root = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") - return os.path.join(root, "src") + src_dir = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") + else: + src_dir = os.path.join(root, "src") + return src_dir def ensure_reqs(reqs): @@ -208,22 +210,70 @@ def get_metadata(path, pkg_name=None): } -@attr.s(slots=True) +@attr.s(slots=True, frozen=True) +class BaseRequirement(object): + name = attr.ib(type=str, default="", cmp=True) + requirement = attr.ib(default=None, cmp=True) # type: Optional[PkgResourcesRequirement] + + def __str__(self): + return "{0}".format(str(self.requirement)) + + def as_dict(self): + return {self.name: self.requirement} + + def as_tuple(self): + return (self.name, self.requirement) + + @classmethod + def from_string(cls, line): + line = line.strip() + req = init_requirement(line) + return cls.from_req(req) + + @classmethod + def from_req(cls, req): + name = None + key = getattr(req, "key", None) + name = getattr(req, "name", None) + project_name = getattr(req, "project_name", None) + if key is not None: + name = key + if name is None: + name = project_name + return cls(name=name, requirement=req) + + +@attr.s(slots=True, cmp=False) class SetupInfo(object): - name = attr.ib(type=str, default=None) - base_dir = attr.ib(type=Path, default=None) - version = attr.ib(type=packaging.version.Version, default=None) - requires = attr.ib(type=dict, default=attr.Factory(dict)) - build_requires = attr.ib(type=list, default=attr.Factory(list)) - build_backend = attr.ib(type=list, default=attr.Factory(list)) - setup_requires = attr.ib(type=dict, default=attr.Factory(list)) - python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None) - extras = attr.ib(type=dict, default=attr.Factory(dict)) - setup_cfg = attr.ib(type=Path, default=None) - setup_py = attr.ib(type=Path, default=None) - pyproject = attr.ib(type=Path, default=None) - ireq = attr.ib(default=None) - extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict) + name = attr.ib(type=str, default=None, cmp=True) + base_dir = attr.ib(type=Path, default=None, cmp=True, hash=False) + version = attr.ib(type=str, default=None, cmp=True) + _requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) + build_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) + build_backend = attr.ib(type=str, default="setuptools.build_meta", cmp=True) + setup_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) + python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None, cmp=True) + _extras_requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) + setup_cfg = attr.ib(type=Path, default=None, cmp=True, hash=False) + setup_py = attr.ib(type=Path, default=None, cmp=True, hash=False) + pyproject = attr.ib(type=Path, default=None, cmp=True, hash=False) + ireq = attr.ib(default=None, cmp=True, hash=False) + extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict, cmp=False, hash=False) + + @property + def requires(self): + return {req.name: req.requirement for req in self._requirements} + + @property + def extras(self): + extras_dict = {} + extras = set(self._extras_requirements) + for section, deps in extras: + if isinstance(deps, BaseRequirement): + extras_dict[section] = deps.requirement + elif isinstance(deps, (list, tuple)): + extras_dict[section] = [d.requirement for d in deps] + return extras_dict @classmethod def get_setup_cfg(cls, setup_cfg_path): @@ -245,29 +295,29 @@ def get_setup_cfg(cls, setup_cfg_path): results["name"] = parser.get("metadata", "name") if parser.has_option("metadata", "version"): results["version"] = parser.get("metadata", "version") - install_requires = {} + install_requires = () if parser.has_option("options", "install_requires"): - install_requires = { - dep.strip(): init_requirement(dep.strip()) + install_requires = tuple([ + BaseRequirement.from_string(dep) for dep in parser.get("options", "install_requires").split("\n") if dep - } + ]) results["install_requires"] = install_requires if parser.has_option("options", "python_requires"): results["python_requires"] = parser.get("options", "python_requires") - extras_require = {} + extras_require = () if "options.extras_require" in parser.sections(): - extras_require = { - section: [ - init_requirement(dep.strip()) + extras_require = tuple([ + (section, tuple([ + BaseRequirement.from_string(dep) for dep in parser.get( "options.extras_require", section ).split("\n") if dep - ] + ])) for section in parser.options("options.extras_require") if section not in ["options", "metadata"] - } + ]) results["extras_require"] = extras_require return results @@ -278,15 +328,18 @@ def parse_setup_cfg(self): self.name = parsed.get("name") if self.version is None: self.version = parsed.get("version") - self.requires.update(parsed["install_requires"]) + self._requirements = self._requirements + parsed["install_requires"] if self.python_requires is None: self.python_requires = parsed.get("python_requires") - self.extras.update(parsed["extras_require"]) + if not self._extras_requirements: + self._extras_requirements = (parsed["extras_require"]) + else: + self._extras_requirements = self._extras_requirements + parsed["extras_require"] if self.ireq is not None and self.ireq.extras: - self.requires.update({ - extra: self.extras[extra] - for extra in self.ireq.extras if extra in self.extras - }) + for extra in self.ireq.extras: + if extra in self.extras: + extras_tuple = tuple([BaseRequirement.from_req(req) for req in self.extras[extra]]) + self._extras_requirements += ((extra, extras_tuple),) def run_setup(self): if self.setup_py is not None and self.setup_py.exists(): @@ -332,8 +385,14 @@ def run_setup(self): self.python_requires = packaging.specifiers.SpecifierSet( dist.python_requires ) + if not self._extras_requirements: + self._extras_requirements = () if dist.extras_require and not self.extras: - self.extras = dist.extras_require + for extra, extra_requires in dist.extras_require: + extras_tuple = tuple( + BaseRequirement.from_req(req) for req in extra_requires + ) + self._extras_requirements += ((extra, extras_tuple),) install_requires = dist.get_requires() if not install_requires: install_requires = dist.install_requires @@ -342,9 +401,11 @@ def run_setup(self): if getattr(self.ireq, "extras", None): for extra in self.ireq.extras: requirements.extend(list(self.extras.get(extra, []))) - self.requires.update({req.key: req for req in requirements}) + self._requirements = self._requirements + tuple([ + BaseRequirement.from_req(req) for req in requirements + ]) if dist.setup_requires and not self.setup_requires: - self.setup_requires = dist.setup_requires + self.setup_requires = tuple(dist.setup_requires) if not self.version: self.version = dist.get_version() @@ -356,18 +417,20 @@ def get_egg_metadata(self): self.name = metadata.get("name", self.name) if not self.version: self.version = metadata.get("version", self.version) - self.requires.update( - {req.key: req for req in metadata.get("requires", {})} - ) + self._requirements = self._requirements + tuple([ + BaseRequirement.from_req(req) for req in metadata.get("requires", []) + ]) if getattr(self.ireq, "extras", None): for extra in self.ireq.extras: extras = metadata.get("extras", {}).get(extra, []) if extras: - extras = ensure_reqs(extras) - self.extras[extra] = set(extras) - self.requires.update( - {req.key: req for req in extras if req is not None} - ) + extras_tuple = tuple([ + BaseRequirement.from_req(req) + for req in ensure_reqs(extras) + if req is not None + ]) + self._extras_requirements += ((extra, extras_tuple),) + self._requirements = self._requirements + extras_tuple def run_pyproject(self): if self.pyproject and self.pyproject.exists(): @@ -377,32 +440,37 @@ def run_pyproject(self): if backend: self.build_backend = backend if requires and not self.build_requires: - self.build_requires = requires + self.build_requires = tuple(requires) def get_info(self): initial_path = os.path.abspath(os.getcwd()) if self.setup_cfg and self.setup_cfg.exists(): try: - self.parse_setup_cfg() + with cd(self.base_dir): + self.parse_setup_cfg() finally: os.chdir(initial_path) if self.setup_py and self.setup_py.exists(): if not self.requires or not self.name: try: - self.run_setup() + with cd(self.base_dir): + self.run_setup() except Exception: - self.get_egg_metadata() + with cd(self.base_dir): + self.get_egg_metadata() finally: os.chdir(initial_path) if not self.requires or not self.name: try: - self.get_egg_metadata() + with cd(self.base_dir): + self.get_egg_metadata() finally: os.chdir(initial_path) if self.pyproject and self.pyproject.exists(): try: - self.run_pyproject() + with cd(self.base_dir): + self.run_pyproject() finally: os.chdir(initial_path) return self.as_dict() diff --git a/src/requirementslib/models/vcs.py b/src/requirementslib/models/vcs.py index aa9e051e..9296f605 100644 --- a/src/requirementslib/models/vcs.py +++ b/src/requirementslib/models/vcs.py @@ -9,7 +9,7 @@ import sys -@attr.s +@attr.s(hash=True) class VCSRepository(object): DEFAULT_RUN_ARGS = None diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py index 213f3d10..eda4d763 100644 --- a/tests/unit/test_requirements.py +++ b/tests/unit/test_requirements.py @@ -30,16 +30,16 @@ 'requests[socks]==1.10', ), ( - {'pinax': { - 'git': 'git://github.com/pinax/pinax.git', - 'ref': '1.4', + {'pinax-user-accounts': { + 'git': 'git://github.com/pinax/pinax-user-accounts.git', + 'ref': 'v2.1.0', 'editable': True, }}, - '-e git+git://github.com/pinax/pinax.git@1.4#egg=pinax', + '-e git+git://github.com/pinax/pinax-user-accounts.git@v2.1.0#egg=pinax-user-accounts', ), ( - {'pinax': {'git': 'git://github.com/pinax/pinax.git', 'ref': '1.4'}}, - 'git+git://github.com/pinax/pinax.git@1.4#egg=pinax', + {'pinax-user-accounts': {'git': 'git://github.com/pinax/pinax-user-accounts.git', 'ref': 'v2.1.0'}}, + 'git+git://github.com/pinax/pinax-user-accounts.git@v2.1.0#egg=pinax-user-accounts', ), ( # Mercurial. {'MyProject': { @@ -130,28 +130,36 @@ ] +def mock_run_requires(cls): + return {} + + @pytest.mark.utils @pytest.mark.parametrize('expected, requirement', DEP_PIP_PAIRS) -def test_convert_from_pip(expected, requirement): - pkg_name = first(expected.keys()) - pkg_pipfile = expected[pkg_name] - if hasattr(pkg_pipfile, 'keys') and 'editable' in pkg_pipfile and not pkg_pipfile['editable']: - del expected[pkg_name]['editable'] - assert Requirement.from_line(requirement).as_pipfile() == expected +def test_convert_from_pip(monkeypatch, expected, requirement): + with monkeypatch.context() as m: + m.setattr(Requirement, "run_requires", mock_run_requires) + pkg_name = first(expected.keys()) + pkg_pipfile = expected[pkg_name] + if hasattr(pkg_pipfile, 'keys') and 'editable' in pkg_pipfile and not pkg_pipfile['editable']: + del expected[pkg_name]['editable'] + assert Requirement.from_line(requirement).as_pipfile() == expected @pytest.mark.to_line @pytest.mark.parametrize( 'requirement, expected', DEP_PIP_PAIRS + DEP_PIP_PAIRS_LEGACY_PIPFILE, ) -def test_convert_from_pipfile(requirement, expected): - pkg_name = first(requirement.keys()) - pkg_pipfile = requirement[pkg_name] - req = Requirement.from_pipfile(pkg_name, pkg_pipfile) - if " (" in expected and expected.endswith(")"): - # To strip out plette[validation] (>=0.1.1) - expected = expected.replace(" (", "").rstrip(")") - assert req.as_line() == expected.lower() if '://' not in expected else expected +def test_convert_from_pipfile(monkeypatch, requirement, expected): + with monkeypatch.context() as m: + m.setattr(Requirement, "run_requires", mock_run_requires) + pkg_name = first(requirement.keys()) + pkg_pipfile = requirement[pkg_name] + req = Requirement.from_pipfile(pkg_name, pkg_pipfile) + if " (" in expected and expected.endswith(")"): + # To strip out plette[validation] (>=0.1.1) + expected = expected.replace(" (", "").rstrip(")") + assert req.as_line() == expected.lower() if '://' not in expected else expected @pytest.mark.requirements @@ -195,16 +203,18 @@ def test_one_way_editable_extras(): @pytest.mark.utils -def test_convert_from_pip_git_uri_normalize(): +def test_convert_from_pip_git_uri_normalize(monkeypatch): """Pip does not parse this correctly, but we can (by converting to ssh://). """ - dep = 'git+git@host:user/repo.git#egg=myname' - dep = Requirement.from_line(dep).as_pipfile() - assert dep == { - 'myname': { - 'git': 'git@host:user/repo.git', + with monkeypatch.context() as m: + m.setattr(Requirement, "run_requires", mock_run_requires) + dep = 'git+git@host:user/repo.git#egg=myname' + dep = Requirement.from_line(dep).as_pipfile() + assert dep == { + 'myname': { + 'git': 'git@host:user/repo.git', + } } - } @pytest.mark.utils @@ -245,7 +255,7 @@ def test_get_requirements(): extras_markers = Requirement.from_line( "requests[security]; os_name=='posix'" ).requirement - assert extras_markers.extras == ['security'] + assert list(extras_markers.extras) == ['security'] assert extras_markers.name == 'requests' assert str(extras_markers.marker) == 'os_name == "posix"' # Test VCS uris get generated correctly, retain git+git@ if supplied that way, and are named according to egg fragment diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 599e0eb9..95220a84 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,6 +5,10 @@ from requirementslib.models.requirements import Requirement +def mock_run_requires(cls): + return {} + + def test_filter_none(): assert utils.filter_none("abc", "") is False assert utils.filter_none("abc", None) is False @@ -64,9 +68,11 @@ def test_format_requirement(): assert utils.format_requirement(ireq) == 'test==1.2' -def test_format_requirement_editable(): - ireq = Requirement.from_line('-e git+git://fake.org/x/y.git#egg=y').as_ireq() - assert utils.format_requirement(ireq) == '-e git+git://fake.org/x/y.git#egg=y' +def test_format_requirement_editable(monkeypatch): + with monkeypatch.context() as m: + m.setattr(Requirement, "run_requires", mock_run_requires) + ireq = Requirement.from_line('-e git+git://fake.org/x/y.git#egg=y').as_ireq() + assert utils.format_requirement(ireq) == '-e git+git://fake.org/x/y.git#egg=y' def test_format_specifier(): From 468a7543008f68edd33b1ea6a31491bcd5f16021 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 29 Jan 2019 18:03:59 -0500 Subject: [PATCH 06/35] Don't run setup_info until needed, mock unpack calls Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 33 +++-- src/requirementslib/models/setup_info.py | 1 - tests/unit/test_requirements.py | 146 ++++++++++++--------- tests/unit/test_utils.py | 2 + 4 files changed, 106 insertions(+), 76 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 75f620e7..feb4c011 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -1749,7 +1749,7 @@ class Requirement(object): vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[str] req = attr.ib(default=None, cmp=True) markers = attr.ib(default=None, cmp=True) - specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) + _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) index = attr.ib(default=None) editable = attr.ib(default=None, cmp=True) hashes = attr.ib(default=attr.Factory(tuple), converter=tuple, cmp=True) # type: Optional[Tuple[str]] @@ -1806,13 +1806,34 @@ def commit_hash(self): commit_hash = repo.get_commit_hash() return commit_hash - @specifiers.default + @_specifiers.default def get_specifiers(self): # type: () -> Optional[str] if self.req and self.req.req and self.req.req.specifier: return specs_to_string(self.req.req.specifier) return "" + @property + def specifiers(self): + # type: () -> Optional[str] + if not self._specifiers and (self.is_file_or_url or self.is_vcs): + if not self.line_instance: + parts = [ + self.req.line_part, + self.extras_as_pip, + self.markers_as_pip, + ] + self.line_instance = Line("".join(parts)) + setup_info = self.line_instance.setup_info + if setup_info is not None: + setup_info = setup_info.as_dict() + self._specifiers = "=={0}".format(setup_info.get("version")) + if self.line_instance and self.line_instance.ireq: + self.line_instance._ireq.specifiers = SpecifierSet(self.specifiers) + if getattr(self.req, "_parsed_line", None) and self.req._parsed_line.ireq: + self.req._parsed_line._ireq.specifiers = SpecifierSet(self.specifiers) + return self._specifiers + @property def is_vcs(self): # type: () -> bool @@ -1945,14 +1966,6 @@ def from_line(cls, line): if hashes: args["hashes"] = tuple(hashes) # type: ignore cls_inst = cls(**args) - if is_direct_url or cls_inst.is_file_or_url or cls_inst.is_vcs: - if not cls_inst.specifiers: - setup_info = cls_inst.run_requires() - cls_inst.specifiers = "=={0}".format(setup_info.get("version")) - if cls_inst.line_instance.ireq: - cls_inst.line_instance._ireq.specifiers = SpecifierSet(cls_inst.specifiers) - if getattr(cls_inst.req, "_parsed_line", None) and cls_inst.req._parsed_line.ireq: - cls_inst.req._parsed_line._ireq.specifiers = SpecifierSet(cls_inst.specifiers) return cls_inst @classmethod diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 6aec5bdb..55453139 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -555,7 +555,6 @@ def from_ireq(cls, ireq, subdir=None, finder=None): created = cls.create( build_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs ) - created.get_info() return created @classmethod diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py index eda4d763..ccc29f8f 100644 --- a/tests/unit/test_requirements.py +++ b/tests/unit/test_requirements.py @@ -3,8 +3,10 @@ import pytest from first import first from requirementslib import Requirement +from requirementslib.models.setup_info import SetupInfo from requirementslib.exceptions import RequirementError from vistir.compat import Path +import pip_shims.shims UNIT_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -134,11 +136,17 @@ def mock_run_requires(cls): return {} +def mock_unpack(link, source_dir, download_dir, only_download=False, session=None, hashes=None, progress_bar="off"): + return + + @pytest.mark.utils @pytest.mark.parametrize('expected, requirement', DEP_PIP_PAIRS) def test_convert_from_pip(monkeypatch, expected, requirement): with monkeypatch.context() as m: m.setattr(Requirement, "run_requires", mock_run_requires) + m.setattr(SetupInfo, "get_info", mock_run_requires) + m.setattr(pip_shims.shims, "unpack_url", mock_unpack) pkg_name = first(expected.keys()) pkg_pipfile = expected[pkg_name] if hasattr(pkg_pipfile, 'keys') and 'editable' in pkg_pipfile and not pkg_pipfile['editable']: @@ -152,6 +160,8 @@ def test_convert_from_pip(monkeypatch, expected, requirement): ) def test_convert_from_pipfile(monkeypatch, requirement, expected): with monkeypatch.context() as m: + m.setattr(pip_shims.shims, "unpack_url", mock_unpack) + m.setattr(SetupInfo, "get_info", mock_run_requires) m.setattr(Requirement, "run_requires", mock_run_requires) pkg_name = first(requirement.keys()) pkg_pipfile = requirement[pkg_name] @@ -208,6 +218,7 @@ def test_convert_from_pip_git_uri_normalize(monkeypatch): """ with monkeypatch.context() as m: m.setattr(Requirement, "run_requires", mock_run_requires) + m.setattr(SetupInfo, "get_info", mock_run_requires) dep = 'git+git@host:user/repo.git#egg=myname' dep = Requirement.from_line(dep).as_pipfile() assert dep == { @@ -219,68 +230,71 @@ def test_convert_from_pip_git_uri_normalize(monkeypatch): @pytest.mark.utils @pytest.mark.requirements -def test_get_requirements(): +def test_get_requirements(monkeypatch): # Test eggs in URLs - url_with_egg = Requirement.from_line( - 'https://github.com/IndustriaTech/django-user-clipboard/archive/0.6.1.zip#egg=django-user-clipboard' - ).requirement - assert url_with_egg.url == 'https://github.com/IndustriaTech/django-user-clipboard/archive/0.6.1.zip' - assert url_with_egg.name == 'django-user-clipboard' - # Test URLs without eggs pointing at installable zipfiles - url = Requirement.from_line( - 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip' - ).requirement - assert url.url == 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip' - wheel_line = "https://github.com/pypa/pipenv/raw/master/tests/test_artifacts/six-1.11.0+mkl-py2.py3-none-any.whl" - wheel = Requirement.from_line(wheel_line) - assert wheel.as_pipfile() == { - "six": {'file': 'https://github.com/pypa/pipenv/raw/master/tests/test_artifacts/six-1.11.0+mkl-py2.py3-none-any.whl'} - } - # Requirementslib inserts egg fragments as names when possible if we know the appropriate name - # this allows for custom naming - assert Requirement.from_pipfile(wheel.name, list(wheel.as_pipfile().values())[0]).as_line().split("#")[0] == wheel_line - # Test VCS urls with refs and eggnames - vcs_url = Requirement.from_line( - 'git+https://github.com/kennethreitz/tablib.git@master#egg=tablib' - ).requirement - assert vcs_url.vcs == 'git' and vcs_url.name == 'tablib' and vcs_url.revision == 'master' - assert vcs_url.url == 'git+https://github.com/kennethreitz/tablib.git' - # Test normal package requirement - normal = Requirement.from_line('tablib').requirement - assert normal.name == 'tablib' - # Pinned package requirement - spec = Requirement.from_line('tablib==0.12.1').requirement - assert spec.name == 'tablib' and spec.specs == [('==', '0.12.1')] - # Test complex package with both extras and markers - extras_markers = Requirement.from_line( - "requests[security]; os_name=='posix'" - ).requirement - assert list(extras_markers.extras) == ['security'] - assert extras_markers.name == 'requests' - assert str(extras_markers.marker) == 'os_name == "posix"' - # Test VCS uris get generated correctly, retain git+git@ if supplied that way, and are named according to egg fragment - git_reformat = Requirement.from_line( - '-e git+git@github.com:pypa/pipenv.git#egg=pipenv' - ).requirement - assert git_reformat.url == 'git+ssh://git@github.com/pypa/pipenv.git' - assert git_reformat.name == 'pipenv' - assert git_reformat.editable - # Previously VCS uris were being treated as local files, so make sure these are not handled that way - assert not git_reformat.local_file - # Test regression where VCS uris were being handled as paths rather than VCS entries - assert git_reformat.vcs == 'git' - assert git_reformat.link.url == 'git+ssh://git@github.com/pypa/pipenv.git#egg=pipenv' - # Test VCS requirements being added with extras for constraint_line - git_extras = Requirement.from_line( - '-e git+https://github.com/requests/requests.git@master#egg=requests[security]' - ) - assert git_extras.as_line() == '-e git+https://github.com/requests/requests.git@master#egg=requests[security]' - assert git_extras.constraint_line == '-e git+https://github.com/requests/requests.git@master#egg=requests[security]' - # these will fail due to not being real paths - # local_wheel = Requirement.from_pipfile('six', {'path': '../wheels/six/six-1.11.0-py2.py3-none-any.whl'}) - # assert local_wheel.as_line() == 'file:///home/hawk/git/wheels/six/six-1.11.0-py2.py3-none-any.whl' - # local_wheel_from_line = Requirement.from_line('../wheels/six/six-1.11.0-py2.py3-none-any.whl') - # assert local_wheel_from_line.as_pipfile() == {'six': {'path': '../wheels/six/six-1.11.0-py2.py3-none-any.whl'}} + with monkeypatch.context() as m: + m.setattr(pip_shims.shims, "unpack_url", mock_unpack) + m.setattr(SetupInfo, "get_info", mock_run_requires) + url_with_egg = Requirement.from_line( + 'https://github.com/IndustriaTech/django-user-clipboard/archive/0.6.1.zip#egg=django-user-clipboard' + ).requirement + assert url_with_egg.url == 'https://github.com/IndustriaTech/django-user-clipboard/archive/0.6.1.zip' + assert url_with_egg.name == 'django-user-clipboard' + # Test URLs without eggs pointing at installable zipfiles + url = Requirement.from_line( + 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip' + ).requirement + assert url.url == 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip' + wheel_line = "https://github.com/pypa/pipenv/raw/master/tests/test_artifacts/six-1.11.0+mkl-py2.py3-none-any.whl" + wheel = Requirement.from_line(wheel_line) + assert wheel.as_pipfile() == { + "six": {'file': 'https://github.com/pypa/pipenv/raw/master/tests/test_artifacts/six-1.11.0+mkl-py2.py3-none-any.whl'} + } + # Requirementslib inserts egg fragments as names when possible if we know the appropriate name + # this allows for custom naming + assert Requirement.from_pipfile(wheel.name, list(wheel.as_pipfile().values())[0]).as_line().split("#")[0] == wheel_line + # Test VCS urls with refs and eggnames + vcs_url = Requirement.from_line( + 'git+https://github.com/kennethreitz/tablib.git@master#egg=tablib' + ).requirement + assert vcs_url.vcs == 'git' and vcs_url.name == 'tablib' and vcs_url.revision == 'master' + assert vcs_url.url == 'git+https://github.com/kennethreitz/tablib.git' + # Test normal package requirement + normal = Requirement.from_line('tablib').requirement + assert normal.name == 'tablib' + # Pinned package requirement + spec = Requirement.from_line('tablib==0.12.1').requirement + assert spec.name == 'tablib' and spec.specs == [('==', '0.12.1')] + # Test complex package with both extras and markers + extras_markers = Requirement.from_line( + "requests[security]; os_name=='posix'" + ).requirement + assert list(extras_markers.extras) == ['security'] + assert extras_markers.name == 'requests' + assert str(extras_markers.marker) == 'os_name == "posix"' + # Test VCS uris get generated correctly, retain git+git@ if supplied that way, and are named according to egg fragment + git_reformat = Requirement.from_line( + '-e git+git@github.com:pypa/pipenv.git#egg=pipenv' + ).requirement + assert git_reformat.url == 'git+ssh://git@github.com/pypa/pipenv.git' + assert git_reformat.name == 'pipenv' + assert git_reformat.editable + # Previously VCS uris were being treated as local files, so make sure these are not handled that way + assert not git_reformat.local_file + # Test regression where VCS uris were being handled as paths rather than VCS entries + assert git_reformat.vcs == 'git' + assert git_reformat.link.url == 'git+ssh://git@github.com/pypa/pipenv.git#egg=pipenv' + # Test VCS requirements being added with extras for constraint_line + git_extras = Requirement.from_line( + '-e git+https://github.com/requests/requests.git@master#egg=requests[security]' + ) + assert git_extras.as_line() == '-e git+https://github.com/requests/requests.git@master#egg=requests[security]' + assert git_extras.constraint_line == '-e git+https://github.com/requests/requests.git@master#egg=requests[security]' + # these will fail due to not being real paths + # local_wheel = Requirement.from_pipfile('six', {'path': '../wheels/six/six-1.11.0-py2.py3-none-any.whl'}) + # assert local_wheel.as_line() == 'file:///home/hawk/git/wheels/six/six-1.11.0-py2.py3-none-any.whl' + # local_wheel_from_line = Requirement.from_line('../wheels/six/six-1.11.0-py2.py3-none-any.whl') + # assert local_wheel_from_line.as_pipfile() == {'six': {'path': '../wheels/six/six-1.11.0-py2.py3-none-any.whl'}} def test_get_ref(): @@ -305,10 +319,12 @@ def test_stdout_is_suppressed(capsys, tmpdir): assert err.strip() == "", err -def test_local_editable_ref(): - path = Path(ARTIFACTS_DIR) / 'git/requests' - req = Requirement.from_pipfile("requests", {"editable": True, "git": path.as_uri(), "ref": "2.18.4"}) - assert req.as_line() == "-e git+{0}@2.18.4#egg=requests".format(path.as_uri()) +def test_local_editable_ref(monkeypatch): + with monkeypatch.context() as m: + m.setattr(pip_shims.shims, "unpack_url", mock_unpack) + path = Path(ARTIFACTS_DIR) / 'git/requests' + req = Requirement.from_pipfile("requests", {"editable": True, "git": path.as_uri(), "ref": "2.18.4"}) + assert req.as_line() == "-e git+{0}@2.18.4#egg=requests".format(path.as_uri()) def test_pep_508(): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 95220a84..105cc3b4 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -3,6 +3,7 @@ from requirementslib import utils as base_utils from requirementslib.models import utils from requirementslib.models.requirements import Requirement +from requirementslib.models.setup_info import SetupInfo def mock_run_requires(cls): @@ -70,6 +71,7 @@ def test_format_requirement(): def test_format_requirement_editable(monkeypatch): with monkeypatch.context() as m: + m.setattr(SetupInfo, "get_info", mock_run_requires) m.setattr(Requirement, "run_requires", mock_run_requires) ireq = Requirement.from_line('-e git+git://fake.org/x/y.git#egg=y').as_ireq() assert utils.format_requirement(ireq) == '-e git+git://fake.org/x/y.git#egg=y' From f2c2fb949c08c0eaf4544ae9b6903888902b44ef Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 29 Jan 2019 19:31:23 -0500 Subject: [PATCH 07/35] Don't clone using ssh, patch around it' Signed-off-by: Dan Ryan --- .travis.yml | 6 +++++- tests/unit/test_requirements.py | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8dc94096..67af7269 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,11 @@ language: python sudo: false -cache: pip dist: trusty +cache: + directories: + - $HOME/.cache/pipenv + - $HOME/.cache/pip-tools + - $HOME/.cache/pip matrix: fast_finish: true diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py index ccc29f8f..09cbc7e8 100644 --- a/tests/unit/test_requirements.py +++ b/tests/unit/test_requirements.py @@ -173,15 +173,17 @@ def test_convert_from_pipfile(monkeypatch, requirement, expected): @pytest.mark.requirements -def test_convert_from_pipfile_vcs(): +def test_convert_from_pipfile_vcs(monkeypatch): """ssh VCS links should be converted correctly""" - pkg_name = "shellingham" - pkg_pipfile = {"editable": True, "git": "git@github.com:sarugaku/shellingham.git"} - req = Requirement.from_pipfile(pkg_name, pkg_pipfile) - assert ( - req.req.link.url == - "git+ssh://git@github.com/sarugaku/shellingham.git#egg=shellingham" - ) + with monkeypatch.context() as m: + m.setattr(pip_shims.shims, "unpack_url", mock_unpack) + pkg_name = "shellingham" + pkg_pipfile = {"editable": True, "git": "git@github.com:sarugaku/shellingham.git"} + req = Requirement.from_pipfile(pkg_name, pkg_pipfile) + assert ( + req.req.link.url == + "git+ssh://git@github.com/sarugaku/shellingham.git#egg=shellingham" + ) @pytest.mark.utils From 6f24dc333a9ee4240b035c21b50280efebc4a3fd Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 29 Jan 2019 20:23:01 -0500 Subject: [PATCH 08/35] Update travis config Signed-off-by: Dan Ryan --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 67af7269..7a834037 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ jobs: - stage: packaging python: "3.6" install: - - "python -m pip install --upgrade readme-renderer[md] twine" + - "python -m pip install --upgrade readme-renderer[md] twine setuptools requests[security]" script: - "python setup.py sdist" - "twine check dist/*" From 81a848ebdd108052ce4b3adf64e5990159e5cb3c Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 30 Jan 2019 02:01:18 -0500 Subject: [PATCH 09/35] Add more shared state - Share `setup_info`, `parsed_line`, etc - Add more typehints Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 234 ++++++++++++++++----- src/requirementslib/models/setup_info.py | 3 +- src/requirementslib/models/utils.py | 8 +- 3 files changed, 192 insertions(+), 53 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index feb4c011..8cfd7707 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -191,6 +191,28 @@ def pyproject_toml(self): self.populate_setup_paths() return self._pyproject_toml + @property + def specifiers(self): + # type: () -> Optional[SpecifierSet] + if self.ireq is not None and self.ireq.req is not None: + return self.ireq.req.specifier + elif self.requirement is not None: + return self.requirement.specifier + return None + + @specifiers.setter + def specifiers(self, specifiers): + # type: (Union[str, SpecifierSet]) -> None + if type(specifiers) is not SpecifierSet: + if type(specifiers) in six.string_types: + specifiers = SpecifierSet(specifiers) + else: + raise TypeError("Must pass a string or a SpecifierSet") + if self.ireq is not None and self.ireq.req is not None: + self._ireq.req.specifier = specifiers + if self.requirement is not None: + self.requirement.specifier = specifiers + def populate_setup_paths(self): # type: () -> None if not self.link and not self.path: @@ -396,7 +418,7 @@ def is_installable(self): @property def setup_info(self): # type: () -> Optional[SetupInfo] - if self._setup_info is None: + if self._setup_info is None and not self.is_named: self._setup_info = SetupInfo.from_ireq(self.ireq) return self._setup_info @@ -464,6 +486,8 @@ def get_ireq(self): ireq.extras = set(self.extras) if self.parsed_marker is not None and not ireq.markers: ireq.markers = self.parsed_marker + if not ireq.req and self.requirement is not None: + ireq.req = PackagingRequirement(str(self.requirement)) return ireq def parse_ireq(self): @@ -488,6 +512,8 @@ def _parse_wheel(self): def _parse_name_from_link(self): # type: () -> Optional[str] + if self.link is None: + return None if getattr(self.link, "egg_fragment", None): return self.link.egg_fragment elif self.is_wheel: @@ -646,6 +672,7 @@ class NamedRequirement(object): req = attr.ib() # type: PackagingRequirement extras = attr.ib(default=attr.Factory(list)) # type: Tuple[str] editable = attr.ib(default=False) # type: bool + _parsed_line = attr.ib(default=None) # type: Optional[Line] @req.default def get_requirement(self): @@ -655,9 +682,16 @@ def get_requirement(self): ) return req + @property + def parsed_line(self): + # type: () -> Optional[Line] + if self._parsed_line is None: + self._parsed_line = Line(self.line_part) + return self._parsed_line + @classmethod - def from_line(cls, line): - # type: (str) -> NamedRequirement + def from_line(cls, line, parsed_line=None): + # type: (str, Optional[Line]) -> NamedRequirement req = init_requirement(line) specifiers = None # type: Optional[str] if req.specifier: @@ -670,11 +704,18 @@ def from_line(cls, line): if not name: name = getattr(req, "key", line) req.name = name + creation_kwargs = { + "name": name, + "version": specifiers, + "req": req, + "parsed_line": parsed_line, + "extras": None + } extras = None # type: Optional[Tuple[str]] if req.extras: extras = list(req.extras) - return cls(name=name, version=specifiers, req=req, extras=extras) - return cls(name=name, version=specifiers, req=req) + creation_kwargs["extras"] = extras + return cls(**creation_kwargs) @classmethod def from_pipfile(cls, name, pipfile): @@ -707,6 +748,8 @@ def pipfile_part(self): pipfile_dict = attr.asdict(self, filter=filter_none).copy() # type: ignore if "version" not in pipfile_dict: pipfile_dict["version"] = "*" + if "_parsed_line" in pipfile_dict: + pipfile_dict.pop("_parsed_line") name = pipfile_dict.pop("name") return {name: pipfile_dict} @@ -716,7 +759,7 @@ def pipfile_part(self): ) -@attr.s(slots=True, hash=True) +@attr.s(slots=True, cmp=True) class FileRequirement(object): """File requirements for tar.gz installable files or wheels or setup.py containing directories.""" @@ -741,7 +784,7 @@ class FileRequirement(object): #: PyProject Path pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[str] #: Setup metadata e.g. dependencies - setup_info = attr.ib(default=None, cmp=True, hash=True) # type: Optional[SetupInfo] + setup_info = attr.ib(default=None, cmp=True) # type: Optional[SetupInfo] _has_hashed_name = attr.ib(default=False, cmp=True) # type: bool _parsed_line = attr.ib(default=None, hash=True) # type: Optional[Line] #: Package name @@ -875,6 +918,11 @@ def dependencies(self): build_deps = list(set(build_deps)) return deps, setup_deps, build_deps + def __attrs_post_init__(self): + if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: + if self.req is not None: + self._parsed_line._ireq.req = self.req + @uri.default def get_uri(self): # type: () -> str @@ -930,7 +978,7 @@ def get_name(self): setupinfo = self.setup_info elif self._parsed_line is not None and self._parsed_line.setup_info is not None: setupinfo = self._parsed_line.setup_info - self._setup_info = self._parsed_line.setup_info + self.setup_info = self._parsed_line.setup_info else: setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir) if setupinfo: @@ -999,6 +1047,13 @@ def get_requirement(self): req.link = self.link return req + @property + def parsed_line(self): + # type: () -> Optional[Line] + if self._parsed_line is None: + self._parsed_line = Line(self.line_part) + return self._parsed_line + @property def is_local(self): # type: () -> bool @@ -1060,10 +1115,10 @@ def create( uri_scheme=None, # type: str setup_path=None, # type: Optional[Any] relpath=None, # type: Optional[Any] + parsed_line=None, # type: Optional[Line] ): # type: (...) -> FileRequirement - parsed_line = None - if line: + if parsed_line is None and line is not None: parsed_line = Line(line) if relpath and not path: path = relpath @@ -1178,7 +1233,7 @@ def create( if parsed_line and parsed_line.name: if name and len(parsed_line.name) != 7 and len(name) == 7: name = parsed_line.name - if parsed_line and parsed_line.setup_info is not None: + if not creation_kwargs.get("setup_info") and parsed_line and parsed_line.setup_info is not None: creation_kwargs["setup_info"] = parsed_line.setup_info if name: creation_kwargs["name"] = name @@ -1187,11 +1242,14 @@ def create( cls_inst._parsed_line = parsed_line if not cls_inst._parsed_line: cls_inst._parsed_line = Line(cls_inst.line_part) + if cls_inst._parsed_line and cls_inst.parsed_line.ireq and not cls_inst.parsed_line.ireq.req: + if cls_inst.req: + cls_inst._parsed_line._ireq.req = cls_inst.req return cls_inst @classmethod - def from_line(cls, line, extras=None): - # type: (str, Optional[Tuple[str]]) -> FileRequirement + def from_line(cls, line, extras=None, parsed_line=None): + # type: (str, Optional[Tuple[str]], Optional[Line]) -> FileRequirement line = line.strip('"').strip("'") link = None path = None @@ -1226,6 +1284,8 @@ def from_line(cls, line, extras=None): } if req is not None: arg_dict["req"] = req + if parsed_line is not None: + arg_dict["parsed_line"] = parsed_line if link and link.is_wheel: from pip_shims import Wheel @@ -1390,6 +1450,7 @@ class VCSRequirement(FileRequirement): req = attr.ib() def __attrs_post_init__(self): + # type: () -> None if not self.uri: if self.path: self.uri = pip_shims.shims.path_to_url(self.path) @@ -1402,9 +1463,14 @@ def __attrs_post_init__(self): new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) new_uri = "{0}{1}".format(vcs_type, new_uri) self.uri = new_uri + if self.req and ( + self.parsed_line.ireq and not self.parsed_line.ireq.req + ): + self.parsed_line._ireq.req = self.req @link.default def get_link(self): + # type: () -> pip_shims.shims.Link uri = self.uri if self.uri else pip_shims.shims.path_to_url(self.path) vcs_uri = build_vcs_uri( self.vcs, @@ -1418,6 +1484,7 @@ def get_link(self): @name.default def get_name(self): + # type: () -> Optional[str] return ( self.link.egg_fragment or self.req.name if getattr(self, "req", None) @@ -1426,6 +1493,7 @@ def get_name(self): @property def vcs_uri(self): + # type: () -> Optional[str] uri = self.uri if not any(uri.startswith("{0}+".format(vcs)) for vcs in VCS_LIST): uri = "{0}+{1}".format(self.vcs, uri) @@ -1433,6 +1501,7 @@ def vcs_uri(self): @req.default def get_requirement(self): + # type: () -> PkgResourcesRequirement name = self.name or self.link.egg_fragment url = None if self.uri: @@ -1536,11 +1605,13 @@ def get_vcs_repo(self, src_dir=None): return vcsrepo def get_commit_hash(self): + # type: () -> str hash_ = None hash_ = self.repo.get_commit_hash() return hash_ def update_repo(self, src_dir=None, ref=None): + # type: (Optional[str], Optional[str]) -> str if ref: self.ref = ref else: @@ -1555,6 +1626,7 @@ def update_repo(self, src_dir=None, ref=None): @contextmanager def locked_vcs_repo(self, src_dir=None): + # type: (Optional[str]) -> Generator[VCSRepository, None, None] if not src_dir: src_dir = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") vcsrepo = self.get_vcs_repo(src_dir=src_dir) @@ -1566,12 +1638,16 @@ def locked_vcs_repo(self, src_dir=None): checkout = self.req.revision if checkout and ref in checkout: self.uri = uri - - yield vcsrepo + orig_repo = self._repo self._repo = vcsrepo + try: + yield vcsrepo + finally: + self._repo = orig_repo @classmethod def from_pipfile(cls, name, pipfile): + # type: (str, Dict[str, Union[List[str], str, bool]]) -> VCSRequirement creation_args = {} pipfile_keys = [ k @@ -1614,13 +1690,21 @@ def from_pipfile(cls, name, pipfile): else: creation_args[key] = pipfile.get(key) creation_args["name"] = name - return cls(**creation_args) + cls_inst = cls(**creation_args) + if cls_inst._parsed_line is None: + cls_inst._parsed_line = Line(cls_inst.line_part) + if cls_inst.req and ( + cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req + ): + cls_inst._parsed_line.ireq.req = cls_inst.req + return cls_inst @classmethod - def from_line(cls, line, editable=None, extras=None): - # type: (str, Optional[bool], Optional[Tuple[str]]) -> VCSRequirement + def from_line(cls, line, editable=None, extras=None, parsed_line=None): + # type: (str, Optional[bool], Optional[Tuple[str]], Optional[Line]) -> VCSRequirement relpath = None - parsed_line = Line(line) + if parsed_line is None: + parsed_line = Line(line) if editable: parsed_line.editable = editable if extras: @@ -1674,7 +1758,7 @@ def from_line(cls, line, editable=None, extras=None): if relpath: creation_args["relpath"] = relpath # return cls.create(**creation_args) - return cls( + cls_inst = cls( name=name, ref=ref, vcs=vcs_type, @@ -1685,10 +1769,17 @@ def from_line(cls, line, editable=None, extras=None): uri=uri, extras=extras, base_line=line, + parsed_line=parsed_line ) + if cls_inst.req and ( + cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req + ): + cls_inst._parsed_line._ireq.req = cls_inst.req + return cls_inst @property def line_part(self): + # type: () -> str """requirements.txt compatible line part sans-extras""" if self.is_local: base_link = self.link @@ -1719,6 +1810,7 @@ def line_part(self): @staticmethod def _choose_vcs_source(pipfile): + # type: (Dict[str, Union[List[str], str, bool]]) -> Dict[str, Union[List[str], str, bool]] src_keys = [k for k in pipfile.keys() if k in ["path", "uri", "file"]] if src_keys: chosen_key = first(src_keys) @@ -1731,6 +1823,7 @@ def _choose_vcs_source(pipfile): @property def pipfile_part(self): + # type: () -> Dict[str, Dict[str, Union[List[str], str, bool]]] excludes = [ "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" @@ -1747,15 +1840,15 @@ def pipfile_part(self): class Requirement(object): name = attr.ib(cmp=True) # type: str vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[str] - req = attr.ib(default=None, cmp=True) - markers = attr.ib(default=None, cmp=True) - _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) - index = attr.ib(default=None) - editable = attr.ib(default=None, cmp=True) + req = attr.ib(default=None, cmp=True) # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] + markers = attr.ib(default=None, cmp=True) # type: Optional[str] + _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[str] + index = attr.ib(default=None) # type: Optional[str] + editable = attr.ib(default=None, cmp=True) # type: Optional[bool] hashes = attr.ib(default=attr.Factory(tuple), converter=tuple, cmp=True) # type: Optional[Tuple[str]] extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[str]] - abstract_dep = attr.ib(default=None, cmp=False) - line_instance = attr.ib(default=None, cmp=True) # type: Optional[Line] + abstract_dep = attr.ib(default=None, cmp=False) # type: Optional[AbstractDependency] + _line_instance = attr.ib(default=None, cmp=True) # type: Optional[Line] _ireq = attr.ib(default=None) # type: Optional[pip_shims.InstallRequirement] def __hash__(self): @@ -1763,13 +1856,16 @@ def __hash__(self): @name.default def get_name(self): + # type: () -> Optional[str] return self.req.name @property def requirement(self): + # type: () -> Optional[PackagingRequirement] return self.req.req def get_hashes_as_pip(self, as_list=False): + # type: () -> Union[str, List[str]] if self.hashes: if as_list: return [HASH_STRING.format(h) for h in self.hashes] @@ -1778,10 +1874,12 @@ def get_hashes_as_pip(self, as_list=False): @property def hashes_as_pip(self): + # type: () -> Union[str, List[str]] self.get_hashes_as_pip() @property def markers_as_pip(self): + # type: () -> str if self.markers: return " ; {0}".format(self.markers).replace('"', "'") @@ -1789,6 +1887,7 @@ def markers_as_pip(self): @property def extras_as_pip(self): + # type: () -> str if self.extras: return "[{0}]".format( ",".join(sorted([extra.lower() for extra in self.extras])) @@ -1813,25 +1912,59 @@ def get_specifiers(self): return specs_to_string(self.req.req.specifier) return "" + @property + def line_instance(self): + # type: () -> Optional[Line] + include_extras = True + include_specifiers = True + if self.is_vcs: + include_extras = False + if self.is_file_or_url or self.is_vcs or not self._specifiers: + include_specifiers = False + + if self._line_instance is None: + parts = [ + self.req.line_part, + self.extras_as_pip if include_extras else "", + self._specifiers if include_specifiers else "", + self.markers_as_pip, + ] + self._line_instance = Line("".join(parts)) + return self._line_instance + @property def specifiers(self): # type: () -> Optional[str] - if not self._specifiers and (self.is_file_or_url or self.is_vcs): - if not self.line_instance: - parts = [ - self.req.line_part, - self.extras_as_pip, - self.markers_as_pip, - ] - self.line_instance = Line("".join(parts)) - setup_info = self.line_instance.setup_info - if setup_info is not None: - setup_info = setup_info.as_dict() - self._specifiers = "=={0}".format(setup_info.get("version")) - if self.line_instance and self.line_instance.ireq: - self.line_instance._ireq.specifiers = SpecifierSet(self.specifiers) - if getattr(self.req, "_parsed_line", None) and self.req._parsed_line.ireq: - self.req._parsed_line._ireq.specifiers = SpecifierSet(self.specifiers) + if self._specifiers: + return self._specifiers + else: + specs = self.get_specifiers() + if specs: + self._specifiers = specs + return specs + if not self._specifiers and self.req and self.req.req and self.req.req.specifier: + self._specifiers = specs_to_string(self.req.req.specifier) + elif self.is_named and not self._specifiers: + self._specifiers = self.req.version + elif self.req.parsed_line.specifiers and not self._specifiers: + self._specifiers = specs_to_string(self.req.parsed_line.specifiers) + elif self.line_instance.specifiers and not self._specifiers: + self._specifiers = specs_to_string(self.line_instance.specifiers) + elif not self._specifiers and (self.is_file_or_url or self.is_vcs): + try: + setupinfo_dict = self.run_requires() + except Exception: + setupinfo_dict = None + if setupinfo_dict is not None: + self._specifiers = "=={0}".format(setupinfo_dict.get("version")) + if self._specifiers: + specset = SpecifierSet(self._specifiers) + if self.line_instance and not self.line_instance.specifiers: + self.line_instance.specifiers = specset + if self.req and self.req.parsed_line and not self.req.parsed_line.specifiers: + self.req._parsed_line.specifiers = specset + if self.req and self.req.req and not self.req.req.specifier: + self.req.req.specifier = specset return self._specifiers @property @@ -1908,9 +2041,9 @@ def from_line(cls, line): (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and not (line_is_vcs or is_vcs(possible_url)) ): - r = FileRequirement.from_line(line_with_prefix, extras=extras) + r = FileRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) elif line_is_vcs: - r = VCSRequirement.from_line(line_with_prefix, extras=extras) + r = VCSRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) if isinstance(r, VCSRequirement): vcs = r.vcs elif line == "." and not is_installable_file(line): @@ -1932,7 +2065,7 @@ def from_line(cls, line): extras = tuple(parse_extras(extras)) if version: name = "{0}{1}".format(name, version) - r = NamedRequirement.from_line(line) + r = NamedRequirement.from_line(line, parsed_line=line_instance) req_markers = None if markers: req_markers = PackagingRequirement("fakepkg; {0}".format(markers)) @@ -2018,7 +2151,6 @@ def from_pipfile(cls, name, pipfile): if any(key in _pipfile for key in ["hash", "hashes"]): args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")]) cls_inst = cls(**args) - cls_inst.line_instance = Line(cls_inst.as_line()) return cls_inst def as_line( @@ -2075,6 +2207,7 @@ def as_line( return line def get_markers(self): + # type: () -> Marker markers = self.markers if markers: fake_pkg = PackagingRequirement("fakepkg; {0}".format(markers)) @@ -2082,8 +2215,9 @@ def get_markers(self): return markers def get_specifier(self): + # type: () -> Union[SpecifierSet, LegacySpecifier] try: - return Specifier(self.specifiers) + return SpecifierSet(self.specifiers) except InvalidSpecifier: return LegacySpecifier(self.specifiers) @@ -2109,7 +2243,9 @@ def constraint_line(self): @property def is_direct_url(self): - return self.is_file_or_url and self.req.is_direct_url + return self.is_file_or_url and self.req.is_direct_url or ( + self.line_instance.is_direct_url or self.req.parsed_line.is_direct_url + ) def as_pipfile(self): good_keys = ( diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 55453139..b2b1d31e 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -504,7 +504,8 @@ def from_requirement(cls, requirement, finder=None): @classmethod def from_ireq(cls, ireq, subdir=None, finder=None): import pip_shims.shims - + if not ireq.link: + return if ireq.link.is_wheel: return if not finder: diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index 90bca388..ffed8ce1 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -24,10 +24,10 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Union, Optional, List, Set, Any, TypeVar + from typing import Union, Optional, List, Set, Any, TypeVar, Tuple from attr import _ValidatorType from pkg_resources import Requirement as PkgResourcesRequirement - from pip_shims import Link + from pip_shims.shims import Link _T = TypeVar("_T") @@ -51,7 +51,7 @@ def create_link(link): if not isinstance(link, six.string_types): raise TypeError("must provide a string to instantiate a new link") - from pip_shims import Link + from pip_shims.shims import Link return Link(link) @@ -194,6 +194,7 @@ def get_pyproject(path): def split_markers_from_line(line): + # type: (str) -> Tuple[str, Optional[str]] """Split markers from a dependency""" if not any(line.startswith(uri_prefix) for uri_prefix in SCHEME_LIST): marker_sep = ";" @@ -207,6 +208,7 @@ def split_markers_from_line(line): def split_vcs_method_from_uri(uri): + # type: (str) -> Tuple[Optional[str], str] """Split a vcs+uri formatted uri into (vcs, uri)""" vcs_start = "{0}+" vcs = first([vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))]) From 10c6e552ea75edef98ce54faf460f852134819d7 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 30 Jan 2019 13:07:13 -0500 Subject: [PATCH 10/35] Add `dist-info` to metadata search, fix tests Signed-off-by: Dan Ryan --- src/requirementslib/models/old_file.py | 2443 ++++++++++++++++++++ src/requirementslib/models/requirements.py | 32 +- src/requirementslib/models/setup_info.py | 76 +- src/requirementslib/models/utils.py | 3 +- tests/unit/test_requirements.py | 18 +- tests/unit/test_setup_info.py | 17 +- tests/unit/test_utils.py | 8 + 7 files changed, 2543 insertions(+), 54 deletions(-) create mode 100644 src/requirementslib/models/old_file.py diff --git a/src/requirementslib/models/old_file.py b/src/requirementslib/models/old_file.py new file mode 100644 index 00000000..845d83df --- /dev/null +++ b/src/requirementslib/models/old_file.py @@ -0,0 +1,2443 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function + +import collections +import copy +import hashlib +import os + +from contextlib import contextmanager +from functools import partial + +import attr +import pep517 +import pep517.wrappers +import pip_shims +import vistir + +from first import first +from packaging.markers import Marker +from packaging.requirements import Requirement as PackagingRequirement +from packaging.specifiers import Specifier, SpecifierSet, LegacySpecifier, InvalidSpecifier +from packaging.utils import canonicalize_name +from six.moves.urllib import parse as urllib_parse +from six.moves.urllib.parse import unquote +from vistir.compat import Path +from vistir.misc import dedup +from vistir.path import ( + create_tracked_tempdir, + get_converted_relative_path, + is_file_url, + is_valid_url, + normalize_path, + mkdir_p +) + +from ..exceptions import RequirementError +from ..utils import ( + VCS_LIST, + is_installable_file, + is_vcs, + ensure_setup_py, + add_ssh_scheme_to_git_uri, + strip_ssh_from_git_uri, + get_setup_paths +) +from .setup_info import SetupInfo, _prepare_wheel_building_kwargs +from .utils import ( + HASH_STRING, + build_vcs_uri, + extras_to_string, + filter_none, + format_requirement, + get_version, + init_requirement, + is_pinned_requirement, + make_install_requirement, + parse_extras, + specs_to_string, + split_markers_from_line, + split_vcs_method_from_uri, + validate_path, + validate_specifiers, + validate_vcs, + normalize_name, + create_link, + get_pyproject, +) + +from ..environment import MYPY_RUNNING + +if MYPY_RUNNING: + from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, NoReturn + from pip_shims.shims import Link, InstallRequirement + RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) + from six.moves.urllib.parse import SplitResult + from .vcs import VCSRepository + + +SPECIFIERS_BY_LENGTH = sorted(list(Specifier._operators.keys()), key=len, reverse=True) + + +run = partial(vistir.misc.run, combine_stderr=False, return_object=True, nospin=True) + + +class Line(object): + def __init__(self, line): + # type: (str) -> None + self.editable = line.startswith("-e ") + if self.editable: + line = line[len("-e "):] + self.line = line + self.hashes = [] # type: List[str] + self.extras = [] # type: List[str] + self.markers = None # type: Optional[str] + self.vcs = None # type: Optional[str] + self.path = None # type: Optional[str] + self.relpath = None # type: Optional[str] + self.uri = None # type: Optional[str] + self._link = None # type: Optional[Link] + self.is_local = False + self.name = None # type: Optional[str] + self.specifier = None # type: Optional[str] + self.parsed_marker = None # type: Optional[Marker] + self.preferred_scheme = None # type: Optional[str] + self.requirement = None # type: Optional[PackagingRequirement] + self.is_direct_url = False # type: bool + self._parsed_url = None # type: Optional[urllib_parse.ParseResult] + self._setup_cfg = None # type: Optional[str] + self._setup_py = None # type: Optional[str] + self._pyproject_toml = None # type: Optional[str] + self._pyproject_requires = None # type: Optional[List[str]] + self._pyproject_backend = None # type: Optional[str] + self._wheel_kwargs = None # type: Dict[str, str] + self._vcsrepo = None # type: Optional[VCSRepository] + self._setup_info = None # type: Optional[SetupInfo] + self._ref = None # type: Optional[str] + self._ireq = None # type: Optional[InstallRequirement] + self._src_root = None # type: Optional[str] + self.dist = None # type: Any + super(Line, self).__init__() + self.parse() + + def __hash__(self): + return hash(( + self.editable, self.line, self.markers, tuple(self.extras), + tuple(self.hashes), self.vcs, self.ireq) + ) + + @classmethod + def split_hashes(cls, line): + # type: (str) -> Tuple[str, List[str]] + if "--hash" not in line: + return line, [] + split_line = line.split() + line_parts = [] # type: List[str] + hashes = [] # type: List[str] + for part in split_line: + if part.startswith("--hash"): + param, _, value = part.partition("=") + hashes.append(value) + else: + line_parts.append(part) + line = " ".join(line_parts) + return line, hashes + + @property + def line_with_prefix(self): + # type: () -> str + line = self.line + if self.is_direct_url: + line = self.link.url + if self.editable: + return "-e {0}".format(line) + return line + + @property + def base_path(self): + # type: () -> Optional[str] + if not self.link and not self.path: + self.parse_link() + if not self.path: + pass + path = normalize_path(self.path) + if os.path.exists(path) and os.path.isdir(path): + path = path + elif os.path.exists(path) and os.path.isfile(path): + path = os.path.dirname(path) + else: + path = None + return path + + @property + def setup_py(self): + # type: () -> Optional[str] + if self._setup_py is None: + self.populate_setup_paths() + return self._setup_py + + @property + def setup_cfg(self): + # type: () -> Optional[str] + if self._setup_cfg is None: + self.populate_setup_paths() + return self._setup_cfg + + @property + def pyproject_toml(self): + # type: () -> Optional[str] + if self._pyproject_toml is None: + self.populate_setup_paths() + return self._pyproject_toml + + @property + def specifiers(self): + # type: () -> Optional[SpecifierSet] + if self.ireq is not None and self.ireq.req is not None: + return self.ireq.req.specifier + elif self.requirement is not None: + return self.requirement.specifier + return None + + @specifiers.setter + def specifiers(self, specifiers): + # type: (Union[str, SpecifierSet]) -> None + if type(specifiers) is not SpecifierSet: + if type(specifiers) in six.string_types: + specifiers = SpecifierSet(specifiers) + else: + raise TypeError("Must pass a string or a SpecifierSet") + if self.ireq is not None and self.ireq.req is not None: + self._ireq.req.specifier = specifiers + if self.requirement is not None: + self.requirement.specifier = specifiers + + def populate_setup_paths(self): + # type: () -> None + if not self.link and not self.path: + self.parse_link() + if not self.path: + return + base_path = self.base_path + if base_path is None: + return + setup_paths = get_setup_paths(self.base_path, subdirectory=self.subdirectory) # type: Dict[str, Optional[str]] + self._setup_py = setup_paths.get("setup_py") + self._setup_cfg = setup_paths.get("setup_cfg") + self._pyproject_toml = setup_paths.get("pyproject_toml") + + @property + def pyproject_requires(self): + # type: () -> Optional[List[str]] + if self._pyproject_requires is None and self.pyproject_toml is not None: + pyproject_requires, pyproject_backend = get_pyproject(self.path) + self._pyproject_requires = pyproject_requires + self._pyproject_backend = pyproject_backend + return self._pyproject_requires + + @property + def pyproject_backend(self): + # type: () -> Optional[str] + if self._pyproject_requires is None and self.pyproject_toml is not None: + pyproject_requires, pyproject_backend = get_pyproject(self.path) + if not pyproject_backend and self.setup_cfg is not None: + setup_dict = SetupInfo.get_setup_cfg(self.setup_cfg) + pyproject_backend = "setuptools.build_meta" + pyproject_requires = setup_dict.get("build_requires", ["setuptools", "wheel"]) + + self._pyproject_requires = pyproject_requires + self._pyproject_backend = pyproject_backend + return self._pyproject_backend + + def parse_hashes(self): + # type: () -> None + """ + Parse hashes from *self.line* and set them on the current object. + :returns: Nothing + :rtype: None + """ + + line, hashes = self.split_hashes(self.line) + self.hashes = hashes + self.line = line + + def parse_extras(self): + # type: () -> None + """ + Parse extras from *self.line* and set them on the current object + :returns: Nothing + :rtype: None + """ + + extras = None + if "@" in self.line: + parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(self.line)) + if not parsed.scheme: + name, _, line = self.line.partition("@") + name = name.strip() + line = line.strip() + if is_vcs(line) or is_valid_url(line): + self.is_direct_url = True + name, extras = pip_shims.shims._strip_extras(name) + self.name = name + self.line = line + else: + self.line, extras = pip_shims.shims._strip_extras(self.line) + else: + self.line, extras = pip_shims.shims._strip_extras(self.line) + if extras is not None: + self.extras = parse_extras(extras) + + def get_url(self): + # type: () -> str + """Sets ``self.name`` if given a **PEP-508** style URL""" + + line = self.line + if self.vcs is not None and self.line.startswith("{0}+".format(self.vcs)): + _, _, _parseable = self.line.partition("+") + parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(_parseable)) + else: + parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(line)) + if "@" in self.line and parsed.scheme == "": + name, _, url = self.line.partition("@") + if self.name is None: + self.name = name + if is_valid_url(url): + self.is_direct_url = True + line = url.strip() + parsed = urllib_parse.urlparse(line) + self._parsed_url = parsed + return line + + @property + def url(self): + # type: () -> Optional[str] + if self.uri is not None: + url = add_ssh_scheme_to_git_uri(self.uri) + url = getattr(self.link, "url_without_fragment", None) + if url is not None: + url = add_ssh_scheme_to_git_uri(unquote(url)) + if url is not None and self._parsed_url is None: + if self.vcs is not None: + _, _, _parseable = url.partition("+") + self._parsed_url = urllib_parse.urlparse(_parseable) + return url + + @property + def link(self): + # type: () -> Link + if self._link is None: + self.parse_link() + return self._link + + @property + def subdirectory(self): + # type: () -> Optional[str] + if self.link is not None: + return self.link.subdirectory_fragment + return "" + + @property + def is_wheel(self): + # type: () -> bool + if self.link is None: + return False + return self.link.is_wheel + + @property + def is_artifact(self): + # type: () -> bool + if self.link is None: + return False + return self.link.is_artifact + + @property + def is_vcs(self): + # type: () -> bool + # Installable local files and installable non-vcs urls are handled + # as files, generally speaking + if is_vcs(self.line) or is_vcs(self.get_url()): + return True + return False + + @property + def is_url(self): + # type: () -> bool + url = self.get_url() + if (is_valid_url(url) or is_file_url(url)): + return True + return False + + @property + def is_path(self): + # type: () -> bool + if self.path and ( + self.path.startswith(".") or os.path.isabs(self.path) or + os.path.exists(self.path) + ): + return True + elif os.path.exists(self.line) or os.path.exists(self.get_url()): + return True + return False + + @property + def is_file(self): + # type: () -> bool + if self.is_path or is_file_url(self.get_url()) or (self._parsed_url and self._parsed_url.scheme == "file"): + return True + return False + + @property + def is_named(self): + # type: () -> bool + return not (self.is_file or self.is_url or self.is_vcs) + + @property + def ref(self): + # type: () -> Optional[str] + if self._ref is None: + if self.relpath and "@" in self.relpath: + self._relpath, _, self._ref = self.relpath.rpartition("@") + return self._ref + + @property + def ireq(self): + # type: () -> Optional[pip_shims.InstallRequirement] + if self._ireq is None: + self.parse_ireq() + return self._ireq + + @property + def is_installable(self): + # type: () -> bool + if is_installable_file(self.line) or is_installable_file(self.get_url()) or is_installable_file(self.path) or is_installable_file(self.base_path): + return True + return False + + @property + def setup_info(self): + # type: () -> Optional[SetupInfo] + if self._setup_info is None and not self.is_named: + self._setup_info = SetupInfo.from_ireq(self.ireq) + self._setup_info.get_info() + return self._setup_info + + def _get_vcsrepo(self): + # type: () -> Optional[VCSRepository] + from .vcs import VCSRepository + checkout_directory = self.wheel_kwargs["src_dir"] # type: ignore + if self.name is not None: + checkout_directory = os.path.join(checkout_directory, self.name) # type: ignore + vcsrepo = VCSRepository( + url=self.link.url, + name=self.name, + ref=self.ref if self.ref else None, + checkout_directory=checkout_directory, + vcs_type=self.vcs, + subdirectory=self.subdirectory, + ) + if not self.link.scheme.startswith("file"): + vcsrepo.obtain() + return vcsrepo + + @property + def vcsrepo(self): + # type: () -> Optional[VCSRepository] + if self._vcsrepo is None: + self._vcsrepo = self._get_vcsrepo() + return self._vcsrepo + + def get_ireq(self): + # type: () -> InstallRequirement + if self.is_named: + ireq = pip_shims.shims.install_req_from_line(self.line) + elif (self.is_file or self.is_url) and not self.is_vcs: + line = self.line + if self.is_direct_url: + line = self.link.url + scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" + local_line = next(iter([ + os.path.dirname(os.path.abspath(f)) for f in [ + self.setup_py, self.setup_cfg, self.pyproject_toml + ] if f is not None + ]), None) + line = local_line if local_line is not None else self.line + if scheme == "path": + if not line and self.base_path is not None: + line = os.path.abspath(self.base_path) + else: + if self.link is not None: + line = self.link.url_without_fragment + else: + if self.uri is not None: + line = self.uri + else: + line = self.path + if self.editable: + ireq = pip_shims.shims.install_req_from_editable(self.link.url) + else: + ireq = pip_shims.shims.install_req_from_line(line) + else: + if self.editable: + ireq = pip_shims.shims.install_req_from_editable(self.link.url) + else: + ireq = pip_shims.shims.install_req_from_line(self.link.url) + if self.extras and not ireq.extras: + ireq.extras = set(self.extras) + if self.parsed_marker is not None and not ireq.markers: + ireq.markers = self.parsed_marker + if not ireq.req and self.requirement is not None: + ireq.req = PackagingRequirement(str(self.requirement)) + return ireq + + def parse_ireq(self): + # type: () -> None + if self._ireq is None: + self._ireq = self.get_ireq() + if self._ireq is not None: + if self.requirement is not None and self._ireq.req is None: + self._ireq.req = self.requirement + + def _parse_wheel(self): + # type: () -> Optional[str] + if not self.is_wheel: + pass + from pip_shims.shims import Wheel + _wheel = Wheel(self.link.filename) + name = _wheel.name + version = _wheel.version + self.specifier = "=={0}".format(version) + return name + + def _parse_name_from_link(self): + # type: () -> Optional[str] + + if self.link is None: + return None + if getattr(self.link, "egg_fragment", None): + return self.link.egg_fragment + elif self.is_wheel: + return self._parse_wheel() + return None + + def _parse_name_from_line(self): + # type: () -> Optional[str] + + if not self.is_named: + pass + name = self.line + specifier_match = next( + iter(spec for spec in SPECIFIERS_BY_LENGTH if spec in self.line), None + ) + if specifier_match is not None: + name, specifier_match, version = name.partition(specifier_match) + self.specifier = "{0}{1}".format(specifier_match, version) + return name + + def parse_name(self): + # type: () -> None + if self.name is None: + name = None + if self.link is not None: + name = self._parse_name_from_link() + if name is None and ( + (self.is_url or self.is_artifact or self.is_vcs) and self._parsed_url + ): + if self._parsed_url.fragment: + _, _, name = self._parsed_url.fragment.partition("egg=") + if "&" in name: + # subdirectory fragments might also be in here + name, _, _ = name.partition("&") + if self.is_named and name is None: + name = self._parse_name_from_line() + if name is not None: + name, extras = pip_shims.shims._strip_extras(name) + if extras is not None and not self.extras: + self.extras = parse_extras(extras) + self.name = name + + def _parse_requirement_from_vcs(self): + # type: () -> Optional[PackagingRequirement] + name = self.name if self.name else self.link.egg_fragment + url = self.uri if self.uri else unquote(self.link.url) + if self.is_direct_url: + url = self.link.url + if not name: + raise ValueError( + "pipenv requires an #egg fragment for version controlled " + "dependencies. Please install remote dependency " + "in the form {0}#egg=.".format(url) + ) + req = init_requirement(canonicalize_name(name)) # type: PackagingRequirement + req.editable = self.editable + if not getattr(req, "url") and self.link: + req.url = url + req.line = self.link.url + if ( + self.uri != unquote(self.link.url_without_fragment) + and "git+ssh://" in self.link.url + and (self.uri is not None and "git+git@" in self.uri) + ): + req.line = self.uri + req.url = self.uri + if self.ref: + if self._vcsrepo is not None: + req.revision = self._vcsrepo.get_commit_hash() + else: + req.revision = self.ref + if self.extras: + req.extras = self.extras + req.vcs = self.vcs + req.link = self.link + if self.path and self.link and self.link.scheme.startswith("file"): + req.local_file = True + req.path = self.path + return req + + def parse_requirement(self): + # type: () -> None + if self.name is None: + self.parse_name() + if self.is_named: + self.requirement = init_requirement(self.line) + elif self.is_vcs: + self.requirement = self._parse_requirement_from_vcs() + if self.name is None and ( + self.requirement is not None and self.requirement.name is not None + ): + self.name = self.requirement.name + if self.name is not None and self.requirement is None: + self.requirement = init_requirement(self.name) + if self.requirement: + if self.parsed_marker is not None: + self.requirement.marker = self.parsed_marker + if self.is_url or self.is_file and (self.link or self.url) and not self.is_vcs: + if self.uri: + self.requirement.url = self.uri + elif self.link: + self.requirement.url = unquote(self.link.url_without_fragment) + else: + self.requirement.url = self.url + if self.extras and not self.requirement.extras: + self.requirement.extras = set(self.extras) + + def parse_link(self): + # type: () -> None + if self.is_file or self.is_url or self.is_vcs: + vcs, prefer, relpath, path, uri, link = FileRequirement.get_link_from_line(self.line) + ref = None + if link is not None and "@" in link.path and uri is not None: + uri, _, ref = uri.rpartition("@") + if relpath is not None and "@" in relpath: + relpath, _, ref = relpath.rpartition("@") + self._ref = ref + self.vcs = vcs + self.preferred_scheme = prefer + self.relpath = relpath + self.path = path + self.uri = uri + if self.is_direct_url and self.name is not None and vcs is not None: + self._link = create_link( + build_vcs_uri(vcs=vcs, uri=uri, ref=ref, extras=self.extras, name=self.name) + ) + else: + self._link = link + + def parse_markers(self): + # type: () -> None + if self.markers: + markers = PackagingRequirement("fakepkg; {0}".format(self.markers)).marker + self.parsed_marker = markers + + def parse(self): + # type: () -> None + self.parse_hashes() + self.line, self.markers = split_markers_from_line(self.line) + self.parse_extras() + self.line = self.line.strip('"').strip("'").strip() + if self.line.startswith("git+file:/") and not self.line.startswith("git+file:///"): + self.line = self.line.replace("git+file:/", "git+file:///") + self.parse_markers() + if self.is_file: + self.populate_setup_paths() + self.parse_link() + self.parse_requirement() + self.parse_ireq() + + +@attr.s(slots=True, hash=True) +class NamedRequirement(object): + name = attr.ib() # type: str + version = attr.ib(validator=attr.validators.optional(validate_specifiers)) # type: Optional[str] + req = attr.ib() # type: PackagingRequirement + extras = attr.ib(default=attr.Factory(list)) # type: Tuple[str] + editable = attr.ib(default=False) # type: bool + _parsed_line = attr.ib(default=None) # type: Optional[Line] + + @req.default + def get_requirement(self): + # type: () -> RequirementType + req = init_requirement( + "{0}{1}".format(canonicalize_name(self.name), self.version) + ) + return req + + @property + def parsed_line(self): + # type: () -> Optional[Line] + if self._parsed_line is None: + self._parsed_line = Line(self.line_part) + return self._parsed_line + + @classmethod + def from_line(cls, line, parsed_line=None): + # type: (str, Optional[Line]) -> NamedRequirement + req = init_requirement(line) + specifiers = None # type: Optional[str] + if req.specifier: + specifiers = specs_to_string(req.specifier) + req.line = line + name = getattr(req, "name", None) + if not name: + name = getattr(req, "project_name", None) + req.name = name + if not name: + name = getattr(req, "key", line) + req.name = name + creation_kwargs = { + "name": name, + "version": specifiers, + "req": req, + "parsed_line": parsed_line, + "extras": None + } + extras = None # type: Optional[Tuple[str]] + if req.extras: + extras = list(req.extras) + creation_kwargs["extras"] = extras + return cls(**creation_kwargs) + + @classmethod + def from_pipfile(cls, name, pipfile): + # type: (str, Dict[str, Union[str, Optional[str], Optional[List[str]]]]) -> NamedRequirement + creation_args = {} # type: Dict[str, Union[Optional[str], Optional[List[str]]]] + if hasattr(pipfile, "keys"): + attr_fields = [field.name for field in attr.fields(cls)] + creation_args = {k: v for k, v in pipfile.items() if k in attr_fields} + creation_args["name"] = name + version = get_version(pipfile) # type: Optional[str] + extras = creation_args.get("extras", None) + creation_args["version"] = version + req = init_requirement("{0}{1}".format(name, version)) + if extras: + req.extras += tuple(extras) + creation_args["req"] = req + return cls(**creation_args) # type: ignore + + @property + def line_part(self): + # type: () -> str + # FIXME: This should actually be canonicalized but for now we have to + # simply lowercase it and replace underscores, since full canonicalization + # also replaces dots and that doesn't actually work when querying the index + return "{0}".format(normalize_name(self.name)) + + @property + def pipfile_part(self): + # type: () -> Dict[str, Any] + pipfile_dict = attr.asdict(self, filter=filter_none).copy() # type: ignore + if "version" not in pipfile_dict: + pipfile_dict["version"] = "*" + if "_parsed_line" in pipfile_dict: + pipfile_dict.pop("_parsed_line") + name = pipfile_dict.pop("name") + return {name: pipfile_dict} + + +LinkInfo = collections.namedtuple( + "LinkInfo", ["vcs_type", "prefer", "relpath", "path", "uri", "link"] +) + + +@attr.s(slots=True, cmp=True) +class FileRequirement(object): + """File requirements for tar.gz installable files or wheels or setup.py + containing directories.""" + + #: Path to the relevant `setup.py` location + setup_path = attr.ib(default=None, cmp=True) # type: Optional[str] + #: path to hit - without any of the VCS prefixes (like git+ / http+ / etc) + path = attr.ib(default=None, cmp=True) # type: Optional[str] + #: Whether the package is editable + editable = attr.ib(default=False, cmp=True) # type: bool + #: Extras if applicable + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[str] + _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[str] + #: URI of the package + uri = attr.ib(cmp=True) # type: Optional[str] + #: Link object representing the package to clone + link = attr.ib(cmp=True) # type: Optional[Link] + #: PyProject Requirements + pyproject_requires = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple + #: PyProject Build System + pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[str] + #: PyProject Path + pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[str] + #: Setup metadata e.g. dependencies + _setup_info = attr.ib(default=None, cmp=True) # type: Optional[SetupInfo] + _has_hashed_name = attr.ib(default=False, cmp=True) # type: bool + _parsed_line = attr.ib(default=None, hash=True) # type: Optional[Line] + #: Package name + name = attr.ib(cmp=True) # type: Optional[str] + #: A :class:`~pkg_resources.Requirement` isntance + req = attr.ib(cmp=True) # type: Optional[PackagingRequirement] + + @classmethod + def get_link_from_line(cls, line): + # type: (str) -> LinkInfo + """Parse link information from given requirement line. + + Return a 6-tuple: + + - `vcs_type` indicates the VCS to use (e.g. "git"), or None. + - `prefer` is either "file", "path" or "uri", indicating how the + information should be used in later stages. + - `relpath` is the relative path to use when recording the dependency, + instead of the absolute path/URI used to perform installation. + This can be None (to prefer the absolute path or URI). + - `path` is the absolute file path to the package. This will always use + forward slashes. Can be None if the line is a remote URI. + - `uri` is the absolute URI to the package. Can be None if the line is + not a URI. + - `link` is an instance of :class:`pip._internal.index.Link`, + representing a URI parse result based on the value of `uri`. + + This function is provided to deal with edge cases concerning URIs + without a valid netloc. Those URIs are problematic to a straight + ``urlsplit` call because they cannot be reliably reconstructed with + ``urlunsplit`` due to a bug in the standard library: + + >>> from urllib.parse import urlsplit, urlunsplit + >>> urlunsplit(urlsplit('git+file:///this/breaks')) + 'git+file:/this/breaks' + >>> urlunsplit(urlsplit('file:///this/works')) + 'file:///this/works' + + See `https://bugs.python.org/issue23505#msg277350`. + """ + + # Git allows `git@github.com...` lines that are not really URIs. + # Add "ssh://" so we can parse correctly, and restore afterwards. + fixed_line = add_ssh_scheme_to_git_uri(line) # type: str + added_ssh_scheme = fixed_line != line # type: bool + + # We can assume a lot of things if this is a local filesystem path. + if "://" not in fixed_line: + p = Path(fixed_line).absolute() # type: Path + path = p.as_posix() # type: Optional[str] + uri = p.as_uri() # type: str + link = create_link(uri) # type: Link + relpath = None # type: Optional[str] + try: + relpath = get_converted_relative_path(path) + except ValueError: + relpath = None + return LinkInfo(None, "path", relpath, path, uri, link) + + # This is an URI. We'll need to perform some elaborated parsing. + + parsed_url = urllib_parse.urlsplit(fixed_line) # type: SplitResult + original_url = parsed_url._replace() # type: SplitResult + + # Split the VCS part out if needed. + original_scheme = parsed_url.scheme # type: str + vcs_type = None # type: Optional[str] + if "+" in original_scheme: + scheme = None # type: Optional[str] + vcs_type, _, scheme = original_scheme.partition("+") + parsed_url = parsed_url._replace(scheme=scheme) + prefer = "uri" # type: str + else: + vcs_type = None + prefer = "file" + + if parsed_url.scheme == "file" and parsed_url.path: + # This is a "file://" URI. Use url_to_path and path_to_url to + # ensure the path is absolute. Also we need to build relpath. + path = Path( + pip_shims.shims.url_to_path(urllib_parse.urlunsplit(parsed_url)) + ).as_posix() + try: + relpath = get_converted_relative_path(path) + except ValueError: + relpath = None + uri = pip_shims.shims.path_to_url(path) + else: + # This is a remote URI. Simply use it. + path = None + relpath = None + # Cut the fragment, but otherwise this is fixed_line. + uri = urllib_parse.urlunsplit( + parsed_url._replace(scheme=original_scheme, fragment="") + ) + + if added_ssh_scheme: + original_uri = urllib_parse.urlunsplit( + original_url._replace(scheme=original_scheme, fragment="") + ) + uri = strip_ssh_from_git_uri(original_uri) + + # Re-attach VCS prefix to build a Link. + link = create_link( + urllib_parse.urlunsplit(parsed_url._replace(scheme=original_scheme)) + ) + + return LinkInfo(vcs_type, prefer, relpath, path, uri, link) + + @property + def setup_info(self): + # type: () -> Optional[SetupInfo] + if self._setup_info is None: + try: + self._setup_info = self.parsed_line.setup_info + except Exception: + self._setup_info = SetupInfo.from_ireq(self.parsed_line.ireq) + if self._setup_info.as_dict().get("requires") is None: + self._setup_info.get_info() + return self._setup_info + + @property + def setup_py_dir(self): + # type: () -> Optional[str] + if self.setup_path: + return os.path.dirname(os.path.abspath(self.setup_path)) + return None + + @property + def dependencies(self): + # type: () -> Tuple[Dict[str, PackagingRequirement], List[Union[str, PackagingRequirement]], List[str]] + build_deps = [] # type: List[Union[str, PackagingRequirement]] + setup_deps = [] # type: List[str] + deps = {} # type: Dict[str, PackagingRequirement] + if self.setup_info: + setup_info = self.setup_info.as_dict() + deps.update(setup_info.get("requires", {})) + setup_deps.extend(setup_info.get("setup_requires", [])) + build_deps.extend(setup_info.get("build_requires", [])) + if self.pyproject_requires: + build_deps.extend(list(self.pyproject_requires)) + setup_deps = list(set(setup_deps)) + build_deps = list(set(build_deps)) + return deps, setup_deps, build_deps + + def __attrs_post_init__(self): + if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: + if self.req is not None: + self._parsed_line._ireq.req = self.req + + @uri.default + def get_uri(self): + # type: () -> str + if self.path and not self.uri: + self._uri_scheme = "path" + return pip_shims.shims.path_to_url(os.path.abspath(self.path)) + elif getattr(self, "req", None) and self.req is not None and getattr(self.req, "url"): + return self.req.url + elif self.link is not None: + return self.link.url_without_fragment + return "" + + @name.default + def get_name(self): + # type: () -> str + loc = self.path or self.uri + if loc and not self._uri_scheme: + self._uri_scheme = "path" if self.path else "file" + name = None + hashed_loc = hashlib.sha256(loc.encode("utf-8")).hexdigest() + hashed_name = hashed_loc[-7:] + if getattr(self, "req", None) and self.req is not None and getattr(self.req, "name") and self.req.name is not None: + if self.is_direct_url and self.req.name != hashed_name: + return self.req.name + if self.link and self.link.egg_fragment and self.link.egg_fragment != hashed_name: + return self.link.egg_fragment + elif self.link and self.link.is_wheel: + from pip_shims import Wheel + self._has_hashed_name = False + return Wheel(self.link.filename).name + elif self.link and ((self.link.scheme == "file" or self.editable) or ( + self.path and self.setup_path and os.path.isfile(str(self.setup_path)) + )): + _ireq = None + if self.editable: + if self.setup_path: + line = pip_shims.shims.path_to_url(self.setup_py_dir) + else: + line = pip_shims.shims.path_to_url(os.path.abspath(self.path)) + if self.extras: + line = "{0}[{1}]".format(line, ",".join(self.extras)) + _ireq = pip_shims.shims.install_req_from_editable(line) + else: + if self.setup_path: + line = Path(self.setup_py_dir).as_posix() + else: + line = Path(os.path.abspath(self.path)).as_posix() + if self.extras: + line = "{0}[{1}]".format(line, ",".join(self.extras)) + _ireq = pip_shims.shims.install_req_from_line(line) + if getattr(self, "req", None) is not None: + _ireq.req = copy.deepcopy(self.req) + if self.extras and _ireq and not _ireq.extras: + _ireq.extras = set(self.extras) + from .setup_info import SetupInfo + subdir = getattr(self, "subdirectory", None) + setupinfo = SetupInfo.from_ireq(self.parsed_line.ireq) + # if self._setup_info is not None: + # setupinfo = self._setup_info + # elif self._parsed_line is not None and self._parsed_line.setup_info is not None: + # setupinfo = self._parsed_line.setup_info + # self._setup_info = self._parsed_line.setup_info + # else: + # setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir) + if setupinfo: + self._setup_info = setupinfo + setupinfo.get_info() + setupinfo_dict = setupinfo.as_dict() + setup_name = setupinfo_dict.get("name", None) + if setup_name: + name = setup_name + self._has_hashed_name = False + build_requires = setupinfo_dict.get("build_requires") + build_backend = setupinfo_dict.get("build_backend") + if build_requires and not self.pyproject_requires: + self.pyproject_requires = tuple(build_requires) + if build_backend and not self.pyproject_backend: + self.pyproject_backend = build_backend + if not name or name.lower() == "unknown": + self._has_hashed_name = True + name = hashed_name + name_in_link = getattr(self.link, "egg_fragment", "") if self.link else "" + if not self._has_hashed_name and name_in_link != name and self.link is not None: + self.link = create_link("{0}#egg={1}".format(self.link.url, name)) + if name is not None: + return name + return "" + + @link.default + def get_link(self): + # type: () -> Link + target = "{0}".format(self.uri) + if hasattr(self, "name") and not self._has_hashed_name: + target = "{0}#egg={1}".format(target, self.name) + link = create_link(target) + return link + + @req.default + def get_requirement(self): + # type: () -> PackagingRequirement + if self.name is None: + if self._parsed_line is not None and self._parsed_line.name is not None: + self.name = self._parsed_line.name + else: + raise ValueError( + "Failed to generate a requirement: missing name for {0!r}".format(self) + ) + req = init_requirement(normalize_name(self.name)) + req.editable = False + if self.link is not None: + req.line = self.link.url_without_fragment + elif self.uri is not None: + req.line = self.uri + else: + req.line = self.name + if self.path and self.link and self.link.scheme.startswith("file"): + req.local_file = True + req.path = self.path + if self.editable: + req.url = None + else: + req.url = self.link.url_without_fragment + else: + req.local_file = False + req.path = None + req.url = self.link.url_without_fragment + if self.editable: + req.editable = True + req.link = self.link + return req + + @property + def parsed_line(self): + # type: () -> Optional[Line] + if self._parsed_line is None: + self._parsed_line = Line(self.line_part) + return self._parsed_line + + @property + def is_local(self): + # type: () -> bool + uri = getattr(self, "uri", None) + if uri is None: + if getattr(self, "path", None) and self.path is not None: + uri = pip_shims.shims.path_to_url(os.path.abspath(self.path)) + elif getattr(self, "req", None) and self.req is not None and ( + getattr(self.req, "url") and self.req.url is not None + ): + uri = self.req.url + if uri and is_file_url(uri): + return True + return False + + @property + def is_remote_artifact(self): + # type: () -> bool + if self.link is None: + return False + return ( + any( + self.link.scheme.startswith(scheme) + for scheme in ("http", "https", "ftp", "ftps", "uri") + ) + and (self.link.is_artifact or self.link.is_wheel) + and not self.editable + ) + + @property + def is_direct_url(self): + # type: () -> bool + if self._parsed_line is not None and self._parsed_line.is_direct_url: + return True + return self.is_remote_artifact + + @property + def formatted_path(self): + # type: () -> Optional[str] + if self.path: + path = self.path + if not isinstance(path, Path): + path = Path(path) + return path.as_posix() + return None + + @classmethod + def create( + cls, + path=None, # type: Optional[str] + uri=None, # type: str + editable=False, # type: bool + extras=None, # type: Optional[Tuple[str]] + link=None, # type: Link + vcs_type=None, # type: Optional[Any] + name=None, # type: Optional[str] + req=None, # type: Optional[Any] + line=None, # type: Optional[str] + uri_scheme=None, # type: str + setup_path=None, # type: Optional[Any] + relpath=None, # type: Optional[Any] + parsed_line=None, # type: Optional[Line] + ): + # type: (...) -> FileRequirement + if parsed_line is None and line is not None: + parsed_line = Line(line) + if relpath and not path: + path = relpath + if not path and uri and link is not None and link.scheme == "file": + path = os.path.abspath(pip_shims.shims.url_to_path(unquote(uri))) + try: + path = get_converted_relative_path(path) + except ValueError: # Vistir raises a ValueError if it can't make a relpath + path = path + if line and not (uri_scheme and uri and link): + vcs_type, uri_scheme, relpath, path, uri, link = cls.get_link_from_line(line) + if not uri_scheme: + uri_scheme = "path" if path else "file" + if path and not uri: + uri = unquote(pip_shims.shims.path_to_url(os.path.abspath(path))) + if not link: + link = cls.get_link_from_line(uri).link + if not uri: + uri = unquote(link.url_without_fragment) + if not extras: + extras = () + pyproject_path = None + pyproject_requires = None + pyproject_backend = None + if path is not None: + pyproject_requires = get_pyproject(path) + if pyproject_requires is not None: + pyproject_requires, pyproject_backend = pyproject_requires + pyproject_requires = tuple(pyproject_requires) + if path: + setup_paths = get_setup_paths(path) + if setup_paths["pyproject_toml"] is not None: + pyproject_path = Path(setup_paths["pyproject_toml"]) + if setup_paths["setup_py"] is not None: + setup_path = Path(setup_paths["setup_py"]).as_posix() + if setup_path and isinstance(setup_path, Path): + setup_path = setup_path.as_posix() + creation_kwargs = { + "editable": editable, + "extras": extras, + "pyproject_path": pyproject_path, + "setup_path": setup_path if setup_path else None, + "uri_scheme": uri_scheme, + "link": link, + "uri": uri, + "pyproject_requires": pyproject_requires, + "pyproject_backend": pyproject_backend, + "path": path or relpath, + "parsed_line": parsed_line + } + if vcs_type: + creation_kwargs["vcs"] = vcs_type + if name: + creation_kwargs["name"] = name + _line = None + ireq = None + setup_info = None + if not name or not parsed_line: + if link is not None and link.url is not None: + _line = unquote(link.url_without_fragment) + if name: + _line = "{0}#egg={1}".format(_line, name) + # if extras: + # _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) + elif uri is not None: + _line = uri + else: + _line = line + if editable: + if extras and ( + (link and link.scheme == "file") or (uri and uri.startswith("file")) + or (not uri and not link) + ): + _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) + if ireq is None: + ireq = pip_shims.shims.install_req_from_editable(_line) + else: + _line = path if (uri_scheme and uri_scheme == "path") else _line + if extras: + _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) + if ireq is None: + ireq = pip_shims.shims.install_req_from_line(_line) + if parsed_line is None: + if editable: + _line = "-e {0}".format(editable) + parsed_line = Line(_line) + if ireq is None: + ireq = parsed_line.ireq + if extras and not ireq.extras: + ireq.extras = set(extras) + if not ireq.is_wheel: + if setup_info is None: + setup_info = SetupInfo.from_ireq(ireq) + setupinfo_dict = setup_info.as_dict() + setup_name = setupinfo_dict.get("name", None) + if setup_name: + name = setup_name + build_requires = setupinfo_dict.get("build_requires", ()) + build_backend = setupinfo_dict.get("build_backend", ()) + if not creation_kwargs.get("pyproject_requires") and build_requires: + creation_kwargs["pyproject_requires"] = tuple(build_requires) + if not creation_kwargs.get("pyproject_backend") and build_backend: + creation_kwargs["pyproject_backend"] = build_backend + creation_kwargs["setup_info"] = setup_info + if path or relpath: + creation_kwargs["path"] = relpath if relpath else path + if req is not None: + creation_kwargs["req"] = req + creation_req = creation_kwargs.get("req") + if creation_kwargs.get("req") is not None: + creation_req_line = getattr(creation_req, "line", None) + if creation_req_line is None and line is not None: + creation_kwargs["req"].line = line # type: ignore + if parsed_line and parsed_line.name: + if name and len(parsed_line.name) != 7 and len(name) == 7: + name = parsed_line.name + if name: + creation_kwargs["name"] = name + cls_inst = cls(**creation_kwargs) # type: ignore + if parsed_line and not cls_inst._parsed_line: + cls_inst._parsed_line = parsed_line + if not cls_inst._parsed_line: + cls_inst._parsed_line = Line(cls_inst.line_part) + if cls_inst._parsed_line and cls_inst.parsed_line.ireq and not cls_inst.parsed_line.ireq.req: + if cls_inst.req: + cls_inst._parsed_line._ireq.req = cls_inst.req + return cls_inst + + @classmethod + def from_line(cls, line, extras=None, parsed_line=None): + # type: (str, Optional[Tuple[str]], Optional[Line]) -> FileRequirement + line = line.strip('"').strip("'") + link = None + path = None + editable = line.startswith("-e ") + line = line.split(" ", 1)[1] if editable else line + setup_path = None + name = None + req = None + if not extras: + extras = () + if not any([is_installable_file(line), is_valid_url(line), is_file_url(line)]): + try: + req = init_requirement(line) + except Exception: + raise RequirementError( + "Supplied requirement is not installable: {0!r}".format(line) + ) + else: + name = getattr(req, "name", None) + line = getattr(req, "url", None) + vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line) + arg_dict = { + "path": relpath if relpath else path, + "uri": unquote(link.url_without_fragment), + "link": link, + "editable": editable, + "setup_path": setup_path, + "uri_scheme": prefer, + "line": line, + "extras": extras, + # "name": name, + } + if req is not None: + arg_dict["req"] = req + if parsed_line is not None: + arg_dict["parsed_line"] = parsed_line + if link and link.is_wheel: + from pip_shims import Wheel + + arg_dict["name"] = Wheel(link.filename).name + elif name: + arg_dict["name"] = name + elif link.egg_fragment: + arg_dict["name"] = link.egg_fragment + return cls.create(**arg_dict) + + @classmethod + def from_pipfile(cls, name, pipfile): + # type: (str, Dict[str, Any]) -> FileRequirement + # Parse the values out. After this dance we should have two variables: + # path - Local filesystem path. + # uri - Absolute URI that is parsable with urlsplit. + # One of these will be a string; the other would be None. + uri = pipfile.get("uri") + fil = pipfile.get("file") + path = pipfile.get("path") + if path: + if isinstance(path, Path) and not path.is_absolute(): + path = get_converted_relative_path(path.as_posix()) + elif not os.path.isabs(path): + path = get_converted_relative_path(path) + if path and uri: + raise ValueError("do not specify both 'path' and 'uri'") + if path and fil: + raise ValueError("do not specify both 'path' and 'file'") + uri = uri or fil + + # Decide that scheme to use. + # 'path' - local filesystem path. + # 'file' - A file:// URI (possibly with VCS prefix). + # 'uri' - Any other URI. + if path: + uri_scheme = "path" + else: + # URI is not currently a valid key in pipfile entries + # see https://github.com/pypa/pipfile/issues/110 + uri_scheme = "file" + + if not uri: + uri = pip_shims.shims.path_to_url(path) + link = cls.get_link_from_line(uri).link + arg_dict = { + "name": name, + "path": path, + "uri": unquote(link.url_without_fragment), + "editable": pipfile.get("editable", False), + "link": link, + "uri_scheme": uri_scheme, + "extras": pipfile.get("extras", None) + } + + extras = pipfile.get("extras", ()) + line = "" + if name: + if extras: + line_name = "{0}[{1}]".format(name, ",".join(sorted(set(extras)))) + else: + line_name = "{0}".format(name) + line = "{0}@ {1}".format(line_name, link.url_without_fragment) + else: + line = link.url + if pipfile.get("editable", False): + line = "-e {0}".format(line) + arg_dict["line"] = line + return cls.create(**arg_dict) + + @property + def line_part(self): + # type: () -> str + link_url = None # type: Optional[str] + seed = None # type: Optional[str] + if self.link is not None: + link_url = unquote(self.link.url_without_fragment) + if self._uri_scheme and self._uri_scheme == "path": + # We may need any one of these for passing to pip + seed = self.path or link_url or self.uri + elif (self._uri_scheme and self._uri_scheme == "file") or ( + (self.link.is_artifact or self.link.is_wheel) and self.link.url + ): + seed = link_url or self.uri + # add egg fragments to remote artifacts (valid urls only) + if not self._has_hashed_name and self.is_remote_artifact and seed is not None: + seed += "#egg={0}".format(self.name) + editable = "-e " if self.editable else "" + if seed is None: + raise ValueError("Could not calculate url for {0!r}".format(self)) + return "{0}{1}".format(editable, seed) + + @property + def pipfile_part(self): + # type: () -> Dict[str, Dict[str, Any]] + excludes = [ + "_base_line", "_has_hashed_name", "setup_path", "pyproject_path", "_uri_scheme", + "pyproject_requires", "pyproject_backend", "setup_info", "_parsed_line" + ] + filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa + pipfile_dict = attr.asdict(self, filter=filter_func).copy() + name = pipfile_dict.pop("name") + if "_uri_scheme" in pipfile_dict: + pipfile_dict.pop("_uri_scheme") + # For local paths and remote installable artifacts (zipfiles, etc) + collision_keys = {"file", "uri", "path"} + collision_order = ["file", "uri", "path"] # type: List[str] + key_match = next(iter(k for k in collision_order if k in pipfile_dict.keys())) + if self._uri_scheme: + dict_key = self._uri_scheme + target_key = ( + dict_key + if dict_key in pipfile_dict + else key_match + ) + if target_key is not None: + winning_value = pipfile_dict.pop(target_key) + collisions = [k for k in collision_keys if k in pipfile_dict] + for key in collisions: + pipfile_dict.pop(key) + pipfile_dict[dict_key] = winning_value + elif ( + self.is_remote_artifact + or (self.link is not None and self.link.is_artifact) + and (self._uri_scheme and self._uri_scheme == "file") + ): + dict_key = "file" + # Look for uri first because file is a uri format and this is designed + # to make sure we add file keys to the pipfile as a replacement of uri + if key_match is not None: + winning_value = pipfile_dict.pop(key_match) + key_to_remove = (k for k in collision_keys if k in pipfile_dict) + for key in key_to_remove: + pipfile_dict.pop(key) + pipfile_dict[dict_key] = winning_value + else: + collisions = [key for key in collision_order if key in pipfile_dict.keys()] + if len(collisions) > 1: + for k in collisions[1:]: + pipfile_dict.pop(k) + return {name: pipfile_dict} + + +@attr.s(slots=True, hash=True) +class VCSRequirement(FileRequirement): + #: Whether the repository is editable + editable = attr.ib(default=None) # type: Optional[bool] + #: URI for the repository + uri = attr.ib(default=None) # type: Optional[str] + #: path to the repository, if it's local + path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) # type: Optional[str] + #: vcs type, i.e. git/hg/svn + vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) # type: Optional[str] + #: vcs reference name (branch / commit / tag) + ref = attr.ib(default=None) # type: Optional[str] + #: Subdirectory to use for installation if applicable + subdirectory = attr.ib(default=None) # type: Optional[str] + _repo = attr.ib(default=None) # type: Optional['VCSRepository'] + _base_line = attr.ib(default=None) # type: Optional[str] + name = attr.ib() + link = attr.ib() + req = attr.ib() + + def __attrs_post_init__(self): + # type: () -> None + if not self.uri: + if self.path: + self.uri = pip_shims.shims.path_to_url(self.path) + split = urllib_parse.urlsplit(self.uri) + scheme, rest = split[0], split[1:] + vcs_type = "" + if "+" in scheme: + vcs_type, scheme = scheme.split("+", 1) + vcs_type = "{0}+".format(vcs_type) + new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) + new_uri = "{0}{1}".format(vcs_type, new_uri) + self.uri = new_uri + if self.req and ( + self.parsed_line.ireq and not self.parsed_line.ireq.req + ): + self.parsed_line._ireq.req = self.req + + @link.default + def get_link(self): + # type: () -> pip_shims.shims.Link + uri = self.uri if self.uri else pip_shims.shims.path_to_url(self.path) + vcs_uri = build_vcs_uri( + self.vcs, + add_ssh_scheme_to_git_uri(uri), + name=self.name, + ref=self.ref, + subdirectory=self.subdirectory, + extras=self.extras, + ) + return self.get_link_from_line(vcs_uri).link + + @name.default + def get_name(self): + # type: () -> Optional[str] + return ( + self.link.egg_fragment or self.req.name + if getattr(self, "req", None) + else super(VCSRequirement, self).get_name() + ) + + @property + def vcs_uri(self): + # type: () -> Optional[str] + uri = self.uri + if not any(uri.startswith("{0}+".format(vcs)) for vcs in VCS_LIST): + uri = "{0}+{1}".format(self.vcs, uri) + return uri + + @req.default + def get_requirement(self): + # type: () -> PkgResourcesRequirement + name = self.name or self.link.egg_fragment + url = None + if self.uri: + url = self.uri + elif self.link is not None: + url = self.link.url_without_fragment + if not name: + raise ValueError( + "pipenv requires an #egg fragment for version controlled " + "dependencies. Please install remote dependency " + "in the form {0}#egg=.".format(url) + ) + req = init_requirement(canonicalize_name(self.name)) + req.editable = self.editable + if not getattr(req, "url"): + if url is not None: + url = add_ssh_scheme_to_git_uri(url) + elif self.uri is not None: + url = self.parse_link_from_line(self.uri).link.url_without_fragment + if url.startswith("git+file:/") and not url.startswith("git+file:///"): + url = url.replace("git+file:/", "git+file:///") + if url: + req.url = url + line = url if url else self.vcs_uri + if self.editable: + line = "-e {0}".format(line) + req.line = line + if self.ref: + req.revision = self.ref + if self.extras: + req.extras = self.extras + req.vcs = self.vcs + if self.path and self.link and self.link.scheme.startswith("file"): + req.local_file = True + req.path = self.path + req.link = self.link + if ( + self.uri != unquote(self.link.url_without_fragment) + and "git+ssh://" in self.link.url + and "git+git@" in self.uri + ): + req.line = self.uri + url = self.link.url_without_fragment + if url.startswith("git+file:/") and not url.startswith("git+file:///"): + url = url.replace("git+file:/", "git+file:///") + req.url = url + return req + + @property + def repo(self): + # type: () -> VCSRepository + if self._repo is None: + self._repo = self.get_vcs_repo() + return self._repo + + def get_checkout_dir(self, src_dir=None): + # type: (Optional[str]) -> str + src_dir = os.environ.get("PIP_SRC", None) if not src_dir else src_dir + checkout_dir = None + if self.is_local: + path = self.path + if not path: + path = pip_shims.shims.url_to_path(self.uri) + if path and os.path.exists(path): + checkout_dir = os.path.abspath(path) + return checkout_dir + if src_dir is not None: + checkout_dir = os.path.join(os.path.abspath(src_dir), self.name) + mkdir_p(src_dir) + return checkout_dir + return os.path.join(create_tracked_tempdir(prefix="requirementslib"), self.name) + + def get_vcs_repo(self, src_dir=None): + # type: (Optional[str]) -> VCSRepository + from .vcs import VCSRepository + + checkout_dir = self.get_checkout_dir(src_dir=src_dir) + vcsrepo = VCSRepository( + url=self.link.url, + name=self.name, + ref=self.ref if self.ref else None, + checkout_directory=checkout_dir, + vcs_type=self.vcs, + subdirectory=self.subdirectory, + ) + if not self.is_local: + vcsrepo.obtain() + pyproject_info = None + if self.subdirectory: + self.setup_path = os.path.join(checkout_dir, self.subdirectory, "setup.py") + self.pyproject_path = os.path.join(checkout_dir, self.subdirectory, "pyproject.toml") + pyproject_info = get_pyproject(os.path.join(checkout_dir, self.subdirectory)) + else: + self.setup_path = os.path.join(checkout_dir, "setup.py") + self.pyproject_path = os.path.join(checkout_dir, "pyproject.toml") + pyproject_info = get_pyproject(checkout_dir) + if pyproject_info is not None: + pyproject_requires, pyproject_backend = pyproject_info + self.pyproject_requires = tuple(pyproject_requires) + self.pyproject_backend = pyproject_backend + return vcsrepo + + def get_commit_hash(self): + # type: () -> str + hash_ = None + hash_ = self.repo.get_commit_hash() + return hash_ + + def update_repo(self, src_dir=None, ref=None): + # type: (Optional[str], Optional[str]) -> str + if ref: + self.ref = ref + else: + if self.ref: + ref = self.ref + repo_hash = None + if not self.is_local and ref is not None: + self.repo.checkout_ref(ref) + repo_hash = self.repo.get_commit_hash() + self.req.revision = repo_hash + return repo_hash + + @contextmanager + def locked_vcs_repo(self, src_dir=None): + # type: (Optional[str]) -> Generator[VCSRepository, None, None] + if not src_dir: + src_dir = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") + vcsrepo = self.get_vcs_repo(src_dir=src_dir) + self.req.revision = vcsrepo.get_commit_hash() + + # Remove potential ref in the end of uri after ref is parsed + if "@" in self.link.show_url and "@" in self.uri: + uri, ref = self.uri.rsplit("@", 1) + checkout = self.req.revision + if checkout and ref in checkout: + self.uri = uri + orig_repo = self._repo + self._repo = vcsrepo + try: + yield vcsrepo + finally: + self._repo = orig_repo + + @classmethod + def from_pipfile(cls, name, pipfile): + # type: (str, Dict[str, Union[List[str], str, bool]]) -> VCSRequirement + creation_args = {} + pipfile_keys = [ + k + for k in ( + "ref", + "vcs", + "subdirectory", + "path", + "editable", + "file", + "uri", + "extras", + ) + + VCS_LIST + if k in pipfile + ] + for key in pipfile_keys: + if key == "extras": + extras = pipfile.get(key, None) + if extras: + pipfile[key] = sorted(dedup([extra.lower() for extra in extras])) + if key in VCS_LIST: + creation_args["vcs"] = key + target = pipfile.get(key) + drive, path = os.path.splitdrive(target) + if ( + not drive + and not os.path.exists(target) + and ( + is_valid_url(target) + or is_file_url(target) + or target.startswith("git@") + ) + ): + creation_args["uri"] = target + else: + creation_args["path"] = target + if os.path.isabs(target): + creation_args["uri"] = pip_shims.shims.path_to_url(target) + else: + creation_args[key] = pipfile.get(key) + creation_args["name"] = name + cls_inst = cls(**creation_args) + if cls_inst._parsed_line is None: + cls_inst._parsed_line = Line(cls_inst.line_part) + if cls_inst.req and ( + cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req + ): + cls_inst._parsed_line.ireq.req = cls_inst.req + return cls_inst + + @classmethod + def from_line(cls, line, editable=None, extras=None, parsed_line=None): + # type: (str, Optional[bool], Optional[Tuple[str]], Optional[Line]) -> VCSRequirement + relpath = None + if parsed_line is None: + parsed_line = Line(line) + if editable: + parsed_line.editable = editable + if extras: + parsed_line.extras = extras + if line.startswith("-e "): + editable = True + line = line.split(" ", 1)[1] + if "@" in line: + parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(line)) + if not parsed.scheme: + possible_name, _, line = line.partition("@") + possible_name = possible_name.strip() + line = line.strip() + possible_name, extras = pip_shims.shims._strip_extras(possible_name) + name = possible_name + line = "{0}#egg={1}".format(line, name) + vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line) + if not extras and link.egg_fragment: + name, extras = pip_shims.shims._strip_extras(link.egg_fragment) + else: + name, _ = pip_shims.shims._strip_extras(link.egg_fragment) + if extras: + extras = parse_extras(extras) + else: + line, extras = pip_shims.shims._strip_extras(line) + if extras: + extras = tuple(extras) + subdirectory = link.subdirectory_fragment + ref = None + if "@" in link.path and "@" in uri: + uri, _, ref = uri.rpartition("@") + if path is not None and "@" in path: + path, _ref = path.rsplit("@", 1) + if ref is None: + ref = _ref + if relpath and "@" in relpath: + relpath, ref = relpath.rsplit("@", 1) + + creation_args = { + "name": name if name else parsed_line.name, + "path": relpath or path, + "editable": editable, + "extras": extras, + "link": link, + "vcs_type": vcs_type, + "line": line, + "uri": uri, + "uri_scheme": prefer, + "parsed_line": parsed_line + } + if relpath: + creation_args["relpath"] = relpath + # return cls.create(**creation_args) + cls_inst = cls( + name=name, + ref=ref, + vcs=vcs_type, + subdirectory=subdirectory, + link=link, + path=relpath or path, + editable=editable, + uri=uri, + extras=extras, + base_line=line, + parsed_line=parsed_line + ) + if cls_inst.req and ( + cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req + ): + cls_inst._parsed_line._ireq.req = cls_inst.req + return cls_inst + + @property + def line_part(self): + # type: () -> str + """requirements.txt compatible line part sans-extras""" + if self.is_local: + base_link = self.link + if not self.link: + base_link = self.get_link() + final_format = ( + "{{0}}#egg={0}".format(base_link.egg_fragment) + if base_link.egg_fragment + else "{0}" + ) + base = final_format.format(self.vcs_uri) + elif self._parsed_line is not None and self._parsed_line.is_direct_url: + return self._parsed_line.line_with_prefix + elif getattr(self, "_base_line", None): + base = self._base_line + else: + base = getattr(self, "link", self.get_link()).url + if base and self.extras and extras_to_string(self.extras) not in base: + if self.subdirectory: + base = "{0}".format(self.get_link().url) + else: + base = "{0}{1}".format(base, extras_to_string(sorted(self.extras))) + if "git+file:/" in base and "git+file:///" not in base: + base = base.replace("git+file:/", "git+file:///") + if self.editable: + base = "-e {0}".format(base) + return base + + @staticmethod + def _choose_vcs_source(pipfile): + # type: (Dict[str, Union[List[str], str, bool]]) -> Dict[str, Union[List[str], str, bool]] + src_keys = [k for k in pipfile.keys() if k in ["path", "uri", "file"]] + if src_keys: + chosen_key = first(src_keys) + vcs_type = pipfile.pop("vcs") + _, pipfile_url = split_vcs_method_from_uri(pipfile.get(chosen_key)) + pipfile[vcs_type] = pipfile_url + for removed in src_keys: + pipfile.pop(removed) + return pipfile + + @property + def pipfile_part(self): + # type: () -> Dict[str, Dict[str, Union[List[str], str, bool]]] + excludes = [ + "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", + "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" + ] + filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa + pipfile_dict = attr.asdict(self, filter=filter_func).copy() + if "vcs" in pipfile_dict: + pipfile_dict = self._choose_vcs_source(pipfile_dict) + name, _ = pip_shims.shims._strip_extras(pipfile_dict.pop("name")) + return {name: pipfile_dict} + + +@attr.s(cmp=True) +class Requirement(object): + name = attr.ib(cmp=True) # type: str + vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[str] + req = attr.ib(default=None, cmp=True) # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] + markers = attr.ib(default=None, cmp=True) # type: Optional[str] + _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[str] + index = attr.ib(default=None) # type: Optional[str] + editable = attr.ib(default=None, cmp=True) # type: Optional[bool] + hashes = attr.ib(default=attr.Factory(tuple), converter=tuple, cmp=True) # type: Optional[Tuple[str]] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[str]] + abstract_dep = attr.ib(default=None, cmp=False) # type: Optional[AbstractDependency] + _line_instance = attr.ib(default=None, cmp=True) # type: Optional[Line] + _ireq = attr.ib(default=None) # type: Optional[pip_shims.InstallRequirement] + + def __hash__(self): + return hash(self.as_line()) + + @name.default + def get_name(self): + # type: () -> Optional[str] + return self.req.name + + @property + def requirement(self): + # type: () -> Optional[PackagingRequirement] + return self.req.req + + def get_hashes_as_pip(self, as_list=False): + # type: () -> Union[str, List[str]] + if self.hashes: + if as_list: + return [HASH_STRING.format(h) for h in self.hashes] + return "".join([HASH_STRING.format(h) for h in self.hashes]) + return "" if not as_list else [] + + @property + def hashes_as_pip(self): + # type: () -> Union[str, List[str]] + self.get_hashes_as_pip() + + @property + def markers_as_pip(self): + # type: () -> str + if self.markers: + return " ; {0}".format(self.markers).replace('"', "'") + + return "" + + @property + def extras_as_pip(self): + # type: () -> str + if self.extras: + return "[{0}]".format( + ",".join(sorted([extra.lower() for extra in self.extras])) + ) + + return "" + + @property + def commit_hash(self): + # type: () -> Optional[str] + if not self.is_vcs: + return None + commit_hash = None + with self.req.locked_vcs_repo() as repo: + commit_hash = repo.get_commit_hash() + return commit_hash + + @_specifiers.default + def get_specifiers(self): + # type: () -> Optional[str] + if self.req and self.req.req and self.req.req.specifier: + return specs_to_string(self.req.req.specifier) + return "" + + @property + def line_instance(self): + # type: () -> Optional[Line] + include_extras = True + include_specifiers = True + if self.is_vcs: + include_extras = False + if self.is_file_or_url or self.is_vcs or not self._specifiers: + include_specifiers = False + + if self._line_instance is None: + parts = [ + self.req.line_part, + self.extras_as_pip if include_extras else "", + self._specifiers if include_specifiers else "", + self.markers_as_pip, + ] + self._line_instance = Line("".join(parts)) + return self._line_instance + + @property + def specifiers(self): + # type: () -> Optional[str] + if self._specifiers: + return self._specifiers + else: + specs = self.get_specifiers() + if specs: + self._specifiers = specs + return specs + if not self._specifiers and self.req and self.req.req and self.req.req.specifier: + self._specifiers = specs_to_string(self.req.req.specifier) + elif self.is_named and not self._specifiers: + self._specifiers = self.req.version + elif self.req.parsed_line.specifiers and not self._specifiers: + self._specifiers = specs_to_string(self.req.parsed_line.specifiers) + elif self.line_instance.specifiers and not self._specifiers: + self._specifiers = specs_to_string(self.line_instance.specifiers) + elif not self._specifiers and (self.is_file_or_url or self.is_vcs): + try: + setupinfo_dict = self.run_requires() + except Exception: + setupinfo_dict = None + if setupinfo_dict is not None: + self._specifiers = "=={0}".format(setupinfo_dict.get("version")) + if self._specifiers: + specset = SpecifierSet(self._specifiers) + if self.line_instance and not self.line_instance.specifiers: + self.line_instance.specifiers = specset + if self.req and self.req.parsed_line and not self.req.parsed_line.specifiers: + self.req._parsed_line.specifiers = specset + if self.req and self.req.req and not self.req.req.specifier: + self.req.req.specifier = specset + return self._specifiers + + @property + def is_vcs(self): + # type: () -> bool + return isinstance(self.req, VCSRequirement) + + @property + def build_backend(self): + # type: () -> Optional[str] + if self.is_vcs or (self.is_file_or_url and self.req.is_local): + setup_info = self.run_requires() + build_backend = setup_info.get("build_backend") + return build_backend + return "setuptools.build_meta" + + @property + def uses_pep517(self): + # type: () -> bool + if self.build_backend: + return True + return False + + @property + def is_file_or_url(self): + # type: () -> bool + return isinstance(self.req, FileRequirement) + + @property + def is_named(self): + # type: () -> bool + return isinstance(self.req, NamedRequirement) + + @property + def normalized_name(self): + return canonicalize_name(self.name) + + def copy(self): + return attr.evolve(self) + + @classmethod + def from_line(cls, line): + # type: (str) -> Requirement + if isinstance(line, pip_shims.shims.InstallRequirement): + line = format_requirement(line) + hashes = None + if "--hash=" in line: + hashes = line.split(" --hash=") + line, hashes = hashes[0], hashes[1:] + line_instance = Line(line) + editable = line.startswith("-e ") + line = line.split(" ", 1)[1] if editable else line + line, markers = split_markers_from_line(line) + line, extras = pip_shims.shims._strip_extras(line) + if extras: + extras = tuple(parse_extras(extras)) + line = line.strip('"').strip("'").strip() + line_with_prefix = "-e {0}".format(line) if editable else line + vcs = None + # Installable local files and installable non-vcs urls are handled + # as files, generally speaking + line_is_vcs = is_vcs(line) + is_direct_url = False + # check for pep-508 compatible requirements + name, _, possible_url = line.partition("@") + name = name.strip() + if possible_url is not None: + possible_url = possible_url.strip() + is_direct_url = is_valid_url(possible_url) + if not line_is_vcs: + line_is_vcs = is_vcs(possible_url) + r = None # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] + if is_installable_file(line) or ( + (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and + not (line_is_vcs or is_vcs(possible_url)) + ): + r = FileRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) + elif line_is_vcs: + r = VCSRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) + if isinstance(r, VCSRequirement): + vcs = r.vcs + elif line == "." and not is_installable_file(line): + raise RequirementError( + "Error parsing requirement %s -- are you sure it is installable?" % line + ) + else: + specs = "!=<>~" + spec_matches = set(specs) & set(line) + version = None + name = "{0}".format(line) + if spec_matches: + spec_idx = min((line.index(match) for match in spec_matches)) + name = line[:spec_idx] + version = line[spec_idx:] + if not extras: + name, extras = pip_shims.shims._strip_extras(name) + if extras: + extras = tuple(parse_extras(extras)) + if version: + name = "{0}{1}".format(name, version) + r = NamedRequirement.from_line(line, parsed_line=line_instance) + req_markers = None + if markers: + req_markers = PackagingRequirement("fakepkg; {0}".format(markers)) + if r is not None and r.req is not None: + r.req.marker = getattr(req_markers, "marker", None) if req_markers else None + r.req.local_file = getattr(r.req, "local_file", False) + name = getattr(r, "name", None) + if name is None and getattr(r.req, "name", None) is not None: + name = r.req.name + elif name is None and getattr(r.req, "key", None) is not None: + name = r.req.key + if name is not None and getattr(r.req, "name", None) is None: + r.req.name = name + args = { + "name": name, + "vcs": vcs, + "req": r, + "markers": markers, + "editable": editable, + "line_instance": line_instance + } + if extras: + extras = tuple(sorted(dedup([extra.lower() for extra in extras]))) + args["extras"] = extras + if r is not None: + r.extras = extras + elif r is not None and r.extras is not None: + args["extras"] = tuple(sorted(dedup([extra.lower() for extra in r.extras]))) # type: ignore + if r.req is not None: + r.req.extras = args["extras"] + if hashes: + args["hashes"] = tuple(hashes) # type: ignore + cls_inst = cls(**args) + return cls_inst + + @classmethod + def from_ireq(cls, ireq): + return cls.from_line(format_requirement(ireq)) + + @classmethod + def from_metadata(cls, name, version, extras, markers): + return cls.from_ireq( + make_install_requirement(name, version, extras=extras, markers=markers) + ) + + @classmethod + def from_pipfile(cls, name, pipfile): + from .markers import PipenvMarkers + + _pipfile = {} + if hasattr(pipfile, "keys"): + _pipfile = dict(pipfile).copy() + _pipfile["version"] = get_version(pipfile) + vcs = first([vcs for vcs in VCS_LIST if vcs in _pipfile]) + if vcs: + _pipfile["vcs"] = vcs + r = VCSRequirement.from_pipfile(name, pipfile) + elif any(key in _pipfile for key in ["path", "file", "uri"]): + r = FileRequirement.from_pipfile(name, pipfile) + else: + r = NamedRequirement.from_pipfile(name, pipfile) + markers = PipenvMarkers.from_pipfile(name, _pipfile) + req_markers = None + if markers: + markers = str(markers) + req_markers = PackagingRequirement("fakepkg; {0}".format(markers)) + if r.req is not None: + r.req.marker = req_markers.marker + extras = _pipfile.get("extras") + r.req.specifier = SpecifierSet(_pipfile["version"]) + r.req.extras = ( + tuple(sorted(dedup([extra.lower() for extra in extras]))) if extras else () + ) + args = { + "name": r.name, + "vcs": vcs, + "req": r, + "markers": markers, + "extras": tuple(_pipfile.get("extras", [])), + "editable": _pipfile.get("editable", False), + "index": _pipfile.get("index"), + } + if any(key in _pipfile for key in ["hash", "hashes"]): + args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")]) + cls_inst = cls(**args) + return cls_inst + + def as_line( + self, + sources=None, + include_hashes=True, + include_extras=True, + include_markers=True, + as_list=False, + ): + """Format this requirement as a line in requirements.txt. + + If ``sources`` provided, it should be an sequence of mappings, containing + all possible sources to be used for this requirement. + + If ``sources`` is omitted or falsy, no index information will be included + in the requirement line. + """ + + include_specifiers = True if self.specifiers else False + if self.is_vcs: + include_extras = False + if self.is_file_or_url or self.is_vcs: + include_specifiers = False + parts = [ + self.req.line_part, + self.extras_as_pip if include_extras else "", + self.specifiers if include_specifiers else "", + self.markers_as_pip if include_markers else "", + ] + if as_list: + # This is used for passing to a subprocess call + parts = ["".join(parts)] + if include_hashes: + hashes = self.get_hashes_as_pip(as_list=as_list) + if as_list: + parts.extend(hashes) + else: + parts.append(hashes) + if sources and not (self.requirement.local_file or self.vcs): + from ..utils import prepare_pip_source_args + + if self.index: + sources = [s for s in sources if s.get("name") == self.index] + source_list = prepare_pip_source_args(sources) + if as_list: + parts.extend(sources) + else: + index_string = " ".join(source_list) + parts.extend([" ", index_string]) + if as_list: + return parts + line = "".join(parts) + return line + + def get_markers(self): + # type: () -> Marker + markers = self.markers + if markers: + fake_pkg = PackagingRequirement("fakepkg; {0}".format(markers)) + markers = fake_pkg.markers + return markers + + def get_specifier(self): + # type: () -> Union[SpecifierSet, LegacySpecifier] + try: + return SpecifierSet(self.specifiers) + except InvalidSpecifier: + return LegacySpecifier(self.specifiers) + + def get_version(self): + return pip_shims.shims.parse_version(self.get_specifier().version) + + def get_requirement(self): + req_line = self.req.req.line + if req_line.startswith("-e "): + _, req_line = req_line.split(" ", 1) + req = init_requirement(self.name) + req.line = req_line + req.specifier = SpecifierSet(self.specifiers if self.specifiers else "") + if self.is_vcs or self.is_file_or_url: + req.url = getattr(self.req.req, "url", self.req.link.url_without_fragment) + req.marker = self.get_markers() + req.extras = set(self.extras) if self.extras else set() + return req + + @property + def constraint_line(self): + return self.as_line() + + @property + def is_direct_url(self): + return self.is_file_or_url and self.req.is_direct_url or ( + self.line_instance.is_direct_url or self.req.parsed_line.is_direct_url + ) + + def as_pipfile(self): + good_keys = ( + "hashes", + "extras", + "markers", + "editable", + "version", + "index", + ) + VCS_LIST + req_dict = { + k: v + for k, v in attr.asdict(self, recurse=False, filter=filter_none).items() + if k in good_keys + } + name = self.name + if "markers" in req_dict and req_dict["markers"]: + req_dict["markers"] = req_dict["markers"].replace('"', "'") + base_dict = { + k: v + for k, v in self.req.pipfile_part[name].items() + if k not in ["req", "link", "setup_info"] + } + base_dict.update(req_dict) + conflicting_keys = ("file", "path", "uri") + if "file" in base_dict and any(k in base_dict for k in conflicting_keys[1:]): + conflicts = [k for k in (conflicting_keys[1:],) if k in base_dict] + for k in conflicts: + base_dict.pop(k) + if "hashes" in base_dict: + _hashes = base_dict.pop("hashes") + hashes = [] + for _hash in _hashes: + try: + hashes.append(_hash.as_line()) + except AttributeError: + hashes.append(_hash) + base_dict["hashes"] = sorted(hashes) + if "extras" in base_dict: + base_dict["extras"] = list(base_dict["extras"]) + if len(base_dict.keys()) == 1 and "version" in base_dict: + base_dict = base_dict.get("version") + return {name: base_dict} + + def as_ireq(self): + if self.line_instance and self.line_instance.ireq: + return self.line_instance.ireq + elif getattr(self.req, "_parsed_line", None) and self.req._parsed_line.ireq: + return self.req._parsed_line.ireq + kwargs = { + "include_hashes": False, + } + if (self.is_file_or_url and self.req.is_local) or self.is_vcs: + kwargs["include_markers"] = False + ireq_line = self.as_line(**kwargs) + ireq = Line(ireq_line).ireq + if not getattr(ireq, "req", None): + ireq.req = self.req.req + if (self.is_file_or_url and self.req.is_local) or self.is_vcs: + if getattr(ireq, "req", None) and getattr(ireq.req, "marker", None): + ireq.req.marker = None + else: + ireq.req.extras = self.req.req.extras + if not ((self.is_file_or_url and self.req.is_local) or self.is_vcs): + ireq.req.marker = self.req.req.marker + return ireq + + @property + def pipfile_entry(self): + return self.as_pipfile().copy().popitem() + + @property + def ireq(self): + return self.as_ireq() + + def get_dependencies(self, sources=None): + """Retrieve the dependencies of the current requirement. + + Retrieves dependencies of the current requirement. This only works on pinned + requirements. + + :param sources: Pipfile-formatted sources, defaults to None + :param sources: list[dict], optional + :return: A set of requirement strings of the dependencies of this requirement. + :rtype: set(str) + """ + + from .dependencies import get_dependencies + + if not sources: + sources = [ + {"name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": True} + ] + return get_dependencies(self.as_ireq(), sources=sources) + + def get_abstract_dependencies(self, sources=None): + """Retrieve the abstract dependencies of this requirement. + + Returns the abstract dependencies of the current requirement in order to resolve. + + :param sources: A list of sources (pipfile format), defaults to None + :param sources: list, optional + :return: A list of abstract (unpinned) dependencies + :rtype: list[ :class:`~requirementslib.models.dependency.AbstractDependency` ] + """ + + from .dependencies import ( + AbstractDependency, + get_dependencies, + get_abstract_dependencies, + ) + + if not self.abstract_dep: + parent = getattr(self, "parent", None) + self.abstract_dep = AbstractDependency.from_requirement(self, parent=parent) + if not sources: + sources = [ + {"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True} + ] + if is_pinned_requirement(self.ireq): + deps = self.get_dependencies() + else: + ireq = sorted(self.find_all_matches(), key=lambda k: k.version) + deps = get_dependencies(ireq.pop(), sources=sources) + return get_abstract_dependencies( + deps, sources=sources, parent=self.abstract_dep + ) + + def find_all_matches(self, sources=None, finder=None): + """Find all matching candidates for the current requirement. + + Consults a finder to find all matching candidates. + + :param sources: Pipfile-formatted sources, defaults to None + :param sources: list[dict], optional + :return: A list of Installation Candidates + :rtype: list[ :class:`~pip._internal.index.InstallationCandidate` ] + """ + + from .dependencies import get_finder, find_all_matches + + if not finder: + finder = get_finder(sources=sources) + return find_all_matches(finder, self.as_ireq()) + + def run_requires(self, sources=None, finder=None): + if self.req and self.req.setup_info is not None: + info_dict = self.req.setup_info.as_dict() + elif self.line_instance and self.line_instance.setup_info is not None: + info_dict = self.line_instance.setup_info.as_dict() + else: + from .setup_info import SetupInfo + if not finder: + from .dependencies import get_finder + finder = get_finder(sources=sources) + info = SetupInfo.from_requirement(self, finder=finder) + if info is None: + return {} + info_dict = info.get_info() + if self.req and not self.req.setup_info: + self.req._setup_info = info + if self.req._has_hashed_name and info_dict.get("name"): + self.req.name = self.name = info_dict["name"] + if self.req.req.name != info_dict["name"]: + self.req.req.name = info_dict["name"] + return info_dict + + def merge_markers(self, markers): + if not isinstance(markers, Marker): + markers = Marker(markers) + _markers = set(Marker(self.ireq.markers)) if self.ireq.markers else set(markers) + _markers.add(markers) + new_markers = Marker(" or ".join([str(m) for m in sorted(_markers)])) + self.markers = str(new_markers) + self.req.req.marker = new_markers diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 8cfd7707..e0d486d4 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -420,6 +420,7 @@ def setup_info(self): # type: () -> Optional[SetupInfo] if self._setup_info is None and not self.is_named: self._setup_info = SetupInfo.from_ireq(self.ireq) + self._setup_info.get_info() return self._setup_info def _get_vcsrepo(self): @@ -919,6 +920,9 @@ def dependencies(self): return deps, setup_deps, build_deps def __attrs_post_init__(self): + if self.setup_info is None and self.parsed_line: + from .setup_info import SetupInfo + self.setup_info = SetupInfo.from_ireq(self.parsed_line.ireq) if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: if self.req is not None: self._parsed_line._ireq.req = self.req @@ -958,12 +962,18 @@ def get_name(self): )): _ireq = None if self.editable: - line = pip_shims.shims.path_to_url(self.setup_py_dir) + if self.setup_path: + line = pip_shims.shims.path_to_url(self.setup_py_dir) + else: + line = pip_shims.shims.path_to_url(os.path.abspath(self.path)) if self.extras: line = "{0}[{1}]".format(line, ",".join(self.extras)) _ireq = pip_shims.shims.install_req_from_editable(line) else: - line = Path(self.setup_py_dir).as_posix() + if self.setup_path: + line = Path(self.setup_py_dir).as_posix() + else: + line = Path(os.path.abspath(self.path)).as_posix() if self.extras: line = "{0}[{1}]".format(line, ",".join(self.extras)) _ireq = pip_shims.shims.install_req_from_line(line) @@ -973,16 +983,13 @@ def get_name(self): _ireq.extras = set(self.extras) from .setup_info import SetupInfo subdir = getattr(self, "subdirectory", None) - setupinfo = None if self.setup_info is not None: setupinfo = self.setup_info - elif self._parsed_line is not None and self._parsed_line.setup_info is not None: - setupinfo = self._parsed_line.setup_info - self.setup_info = self._parsed_line.setup_info else: setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir) if setupinfo: self.setup_info = setupinfo + self.setup_info.get_info() setupinfo_dict = setupinfo.as_dict() setup_name = setupinfo_dict.get("name", None) if setup_name: @@ -1175,6 +1182,7 @@ def create( creation_kwargs["name"] = name _line = None ireq = None + setup_info = None if not name or not parsed_line: if link is not None and link.url is not None: _line = unquote(link.url_without_fragment) @@ -1200,16 +1208,16 @@ def create( _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) if ireq is None: ireq = pip_shims.shims.install_req_from_line(_line) - if parsed_line is None: if editable: _line = "-e {0}".format(editable) - parsed_line = Line(_line) + parsed_line = Line(_line) if ireq is None: ireq = parsed_line.ireq if extras and not ireq.extras: ireq.extras = set(extras) if not ireq.is_wheel: - setup_info = SetupInfo.from_ireq(ireq) + if setup_info is None: + setup_info = SetupInfo.from_ireq(ireq) setupinfo_dict = setup_info.as_dict() setup_name = setupinfo_dict.get("name", None) if setup_name: @@ -1233,8 +1241,6 @@ def create( if parsed_line and parsed_line.name: if name and len(parsed_line.name) != 7 and len(name) == 7: name = parsed_line.name - if not creation_kwargs.get("setup_info") and parsed_line and parsed_line.setup_info is not None: - creation_kwargs["setup_info"] = parsed_line.setup_info if name: creation_kwargs["name"] = name cls_inst = cls(**creation_kwargs) # type: ignore @@ -1467,6 +1473,8 @@ def __attrs_post_init__(self): self.parsed_line.ireq and not self.parsed_line.ireq.req ): self.parsed_line._ireq.req = self.req + if self.setup_info is None and self.parsed_line and self.parsed_line.setup_info: + self.setup_info = self.parsed_line.setup_info @link.default def get_link(self): @@ -1826,7 +1834,7 @@ def pipfile_part(self): # type: () -> Dict[str, Dict[str, Union[List[str], str, bool]]] excludes = [ "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", - "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" + "pyproject_requires", "pyproject_backend", "setup_info", "_parsed_line" ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index b2b1d31e..48d883b9 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -20,7 +20,7 @@ from six.moves import configparser from six.moves.urllib.parse import unquote from vistir.compat import Path, Iterable -from vistir.contextmanagers import cd +from vistir.contextmanagers import cd, temp_environ from vistir.misc import run from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p @@ -133,27 +133,27 @@ def _prepare_wheel_building_kwargs(ireq=None, src_root=None, editable=False): } -def iter_egginfos(path, pkg_name=None): - # type: (str, Optional[str]) -> Generator +def iter_metadata(path, pkg_name=None, metadata_type="egg-info"): + # type: (str, Optional[str], str) -> Generator if pkg_name is not None: pkg_variants = get_name_variants(pkg_name) non_matching_dirs = [] for entry in scandir(path): if entry.is_dir(): entry_name, ext = os.path.splitext(entry.name) - if ext.endswith("egg-info"): - if pkg_name is None or entry_name in pkg_variants: + if ext.endswith(metadata_type): + if pkg_name is None or entry_name.lower() in pkg_variants: yield entry - elif not entry.name.endswith("egg-info"): + elif not entry.name.endswith(metadata_type): non_matching_dirs.append(entry) for entry in non_matching_dirs: - for dir_entry in iter_egginfos(entry.path, pkg_name=pkg_name): + for dir_entry in iter_metadata(entry.path, pkg_name=pkg_name, metadata_type=metadata_type): yield dir_entry def find_egginfo(target, pkg_name=None): # type: (str, Optional[str]) -> Generator - egg_dirs = (egg_dir for egg_dir in iter_egginfos(target, pkg_name=pkg_name)) + egg_dirs = (egg_dir for egg_dir in iter_metadata(target, pkg_name=pkg_name)) if pkg_name: yield next(iter(egg_dirs), None) else: @@ -161,18 +161,38 @@ def find_egginfo(target, pkg_name=None): yield egg_dir +def find_distinfo(target, pkg_name=None): + # type: (str, Optional[str]) -> Generator + dist_dirs = (dist_dir for dist_dir in iter_metadata(target, pkg_name=pkg_name, metadata_type="dist-info")) + if pkg_name: + yield next(iter(dist_dirs), None) + else: + for dist_dir in dist_dirs: + yield dist_dir + + def get_metadata(path, pkg_name=None): egg_dir = next(iter(find_egginfo(path, pkg_name=pkg_name)), None) - if egg_dir is not None: + dist_dir = next(iter(find_distinfo(path, pkg_name=pkg_name)), None) + matched_dir = next(iter(d for d in (dist_dir, egg_dir) if d is not None), None) + metadata_dir = None + base_dir = None + if matched_dir is not None: import pkg_resources - - egg_dir = os.path.abspath(egg_dir.path) - base_dir = os.path.dirname(egg_dir) - path_metadata = pkg_resources.PathMetadata(base_dir, egg_dir) - dist = next( - iter(pkg_resources.distributions_from_metadata(path_metadata.egg_info)), - None, - ) + metadata_dir = os.path.abspath(matched_dir.path) + base_dir = os.path.dirname(metadata_dir) + dist = None + distinfo_dist = None + egg_dist = None + if dist_dir is not None: + distinfo_dist = next(iter(pkg_resources.find_distributions(base_dir)), None) + if egg_dir is not None: + path_metadata = pkg_resources.PathMetadata(base_dir, metadata_dir) + egg_dist = next( + iter(pkg_resources.distributions_from_metadata(path_metadata.egg_info)), + None, + ) + dist = next(iter(d for d in (distinfo_dist, egg_dist) if d is not None), None) if dist: try: requires = dist.requires() @@ -370,6 +390,9 @@ def run_setup(self): python = os.environ.get('PIP_PYTHON_PATH', sys.executable) out, _ = run([python, "setup.py"] + args, cwd=target_cwd, block=True, combine_stderr=False, return_object=False, nospin=True) + except SystemExit: + print("Current directory: %s\nTarget file: %s\nDirectory Contents: %s\nSetup Path Contents: %s\n" % ( + os.getcwd(), script_name, os.listdir(os.getcwd()), os.listdir(os.path.dirname(script_name)))) finally: _setup_stop_after = None sys.argv = save_argv @@ -445,11 +468,8 @@ def run_pyproject(self): def get_info(self): initial_path = os.path.abspath(os.getcwd()) if self.setup_cfg and self.setup_cfg.exists(): - try: - with cd(self.base_dir): - self.parse_setup_cfg() - finally: - os.chdir(initial_path) + with cd(self.base_dir): + self.parse_setup_cfg() if self.setup_py and self.setup_py.exists(): if not self.requires or not self.name: try: @@ -458,14 +478,9 @@ def get_info(self): except Exception: with cd(self.base_dir): self.get_egg_metadata() - finally: - os.chdir(initial_path) if not self.requires or not self.name: - try: - with cd(self.base_dir): - self.get_egg_metadata() - finally: - os.chdir(initial_path) + with cd(self.base_dir): + self.get_egg_metadata() if self.pyproject and self.pyproject.exists(): try: @@ -536,7 +551,8 @@ def from_ireq(cls, ireq, subdir=None, finder=None): "The file URL points to a directory not installable: {}" .format(ireq.link) ) - if not ireq.editable or not ireq.link.scheme == "file": + + if not (ireq.editable and "file" in ireq.link.scheme): pip_shims.shims.unpack_url( ireq.link, ireq.source_dir, diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index ffed8ce1..bf04b354 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -624,7 +624,8 @@ def get_name_variants(pkg): raise TypeError("must provide a string to derive package names") from pkg_resources import safe_name from packaging.utils import canonicalize_name - names = {safe_name(pkg), canonicalize_name(pkg)} + pkg = pkg.lower() + names = {safe_name(pkg), canonicalize_name(pkg), pkg.replace("-", "_")} return names diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py index 09cbc7e8..0ed28c81 100644 --- a/tests/unit/test_requirements.py +++ b/tests/unit/test_requirements.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- import os + +import pip_shims.shims import pytest + from first import first + from requirementslib import Requirement -from requirementslib.models.setup_info import SetupInfo from requirementslib.exceptions import RequirementError +from requirementslib.models.setup_info import SetupInfo from vistir.compat import Path -import pip_shims.shims UNIT_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -32,16 +35,16 @@ 'requests[socks]==1.10', ), ( - {'pinax-user-accounts': { - 'git': 'git://github.com/pinax/pinax-user-accounts.git', + {'django-user-accounts': { + 'git': 'git://github.com/pinax/django-user-accounts.git', 'ref': 'v2.1.0', 'editable': True, }}, - '-e git+git://github.com/pinax/pinax-user-accounts.git@v2.1.0#egg=pinax-user-accounts', + '-e git+git://github.com/pinax/django-user-accounts.git@v2.1.0#egg=django-user-accounts', ), ( - {'pinax-user-accounts': {'git': 'git://github.com/pinax/pinax-user-accounts.git', 'ref': 'v2.1.0'}}, - 'git+git://github.com/pinax/pinax-user-accounts.git@v2.1.0#egg=pinax-user-accounts', + {'django-user-accounts': {'git': 'git://github.com/pinax/django-user-accounts.git', 'ref': 'v2.1.0'}}, + 'git+git://github.com/pinax/django-user-accounts.git@v2.1.0#egg=django-user-accounts', ), ( # Mercurial. {'MyProject': { @@ -221,6 +224,7 @@ def test_convert_from_pip_git_uri_normalize(monkeypatch): with monkeypatch.context() as m: m.setattr(Requirement, "run_requires", mock_run_requires) m.setattr(SetupInfo, "get_info", mock_run_requires) + m.setattr(pip_shims.shims, "unpack_url", mock_unpack) dep = 'git+git@host:user/repo.git#egg=myname' dep = Requirement.from_line(dep).as_pipfile() assert dep == { diff --git a/tests/unit/test_setup_info.py b/tests/unit/test_setup_info.py index 1d59182c..6ef1c210 100644 --- a/tests/unit/test_setup_info.py +++ b/tests/unit/test_setup_info.py @@ -1,8 +1,11 @@ # -*- coding=utf-8 -*- -import pytest -import sys import os +import sys + +import pip_shims.shims +import pytest + import vistir from requirementslib.models.requirements import Requirement @@ -119,6 +122,12 @@ def test_extras(pathlib_tmpdir): with vistir.contextmanagers.cd(pathlib_tmpdir.as_posix()): r = Requirement.from_pipfile("test-package", pipfile_entry) assert r.name == "test-package" - r.run_requires() + r.req.setup_info.get_info() setup_dict = r.req.setup_info.as_dict() - assert sorted(list(setup_dict.get("requires").keys())) == ["coverage", "flaky", "six"] + import sys + for k, v in setup_dict.items(): + print("{0}: {1}".format(k, v), file=sys.stderr) + if k in ("base_dir",): + print(" dir contents: %s" % os.listdir(v)) + # assert setup_dict == "", setup_dict + assert sorted(list(setup_dict.get("requires").keys())) == ["coverage", "flaky", "six"], setup_dict diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 105cc3b4..9dff242a 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,5 +1,8 @@ # -*- coding=utf-8 -*- +import pip_shims.shims + from pytest import raises + from requirementslib import utils as base_utils from requirementslib.models import utils from requirementslib.models.requirements import Requirement @@ -10,6 +13,10 @@ def mock_run_requires(cls): return {} +def mock_unpack(link, source_dir, download_dir, only_download=False, session=None, hashes=None, progress_bar="off"): + return + + def test_filter_none(): assert utils.filter_none("abc", "") is False assert utils.filter_none("abc", None) is False @@ -73,6 +80,7 @@ def test_format_requirement_editable(monkeypatch): with monkeypatch.context() as m: m.setattr(SetupInfo, "get_info", mock_run_requires) m.setattr(Requirement, "run_requires", mock_run_requires) + m.setattr(pip_shims.shims, "unpack_url", mock_unpack) ireq = Requirement.from_line('-e git+git://fake.org/x/y.git#egg=y').as_ireq() assert utils.format_requirement(ireq) == '-e git+git://fake.org/x/y.git#egg=y' From 4d58c94c8e5ccd89337e6164ae8f3329049c0557 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 30 Jan 2019 16:50:17 -0500 Subject: [PATCH 11/35] fix requirement parsing Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 69 +++++++++++++--------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index e0d486d4..ef3e3c36 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -420,7 +420,8 @@ def setup_info(self): # type: () -> Optional[SetupInfo] if self._setup_info is None and not self.is_named: self._setup_info = SetupInfo.from_ireq(self.ireq) - self._setup_info.get_info() + if self._setup_info is not None: + self._setup_info.get_info() return self._setup_info def _get_vcsrepo(self): @@ -785,7 +786,7 @@ class FileRequirement(object): #: PyProject Path pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[str] #: Setup metadata e.g. dependencies - setup_info = attr.ib(default=None, cmp=True) # type: Optional[SetupInfo] + _setup_info = attr.ib(default=None, cmp=True) # type: Optional[SetupInfo] _has_hashed_name = attr.ib(default=False, cmp=True) # type: bool _parsed_line = attr.ib(default=None, hash=True) # type: Optional[Line] #: Package name @@ -920,13 +921,22 @@ def dependencies(self): return deps, setup_deps, build_deps def __attrs_post_init__(self): - if self.setup_info is None and self.parsed_line: - from .setup_info import SetupInfo - self.setup_info = SetupInfo.from_ireq(self.parsed_line.ireq) if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: if self.req is not None: self._parsed_line._ireq.req = self.req + @property + def setup_info(self): + from .setup_info import SetupInfo + if self._setup_info is None and self.parsed_line: + if self.parsed_line.setup_info: + self._setup_info = self.parsed_line.setup_info + elif self.parsed_line.ireq: + self._setup_info = SetupInfo.from_ireq(self.parsed_line.ireq) + else: + self._setup_info = Line(self.line_part).setup_info + return self._setup_info + @uri.default def get_uri(self): # type: () -> str @@ -988,7 +998,7 @@ def get_name(self): else: setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir) if setupinfo: - self.setup_info = setupinfo + self._setup_info = setupinfo self.setup_info.get_info() setupinfo_dict = setupinfo.as_dict() setup_name = setupinfo_dict.get("name", None) @@ -1183,6 +1193,11 @@ def create( _line = None ireq = None setup_info = None + if parsed_line: + if parsed_line.name: + name = parsed_line.name + if parsed_line.setup_info: + name = parsed_line.setup_info.as_dict().get("name", name) if not name or not parsed_line: if link is not None and link.url is not None: _line = unquote(link.url_without_fragment) @@ -1215,20 +1230,21 @@ def create( ireq = parsed_line.ireq if extras and not ireq.extras: ireq.extras = set(extras) - if not ireq.is_wheel: - if setup_info is None: - setup_info = SetupInfo.from_ireq(ireq) - setupinfo_dict = setup_info.as_dict() - setup_name = setupinfo_dict.get("name", None) - if setup_name: - name = setup_name - build_requires = setupinfo_dict.get("build_requires", ()) - build_backend = setupinfo_dict.get("build_backend", ()) - if not creation_kwargs.get("pyproject_requires") and build_requires: - creation_kwargs["pyproject_requires"] = tuple(build_requires) - if not creation_kwargs.get("pyproject_backend") and build_backend: - creation_kwargs["pyproject_backend"] = build_backend - creation_kwargs["setup_info"] = setup_info + if setup_info is None: + setup_info = SetupInfo.from_ireq(ireq) + setupinfo_dict = setup_info.as_dict() + setup_name = setupinfo_dict.get("name", None) + if setup_name: + name = setup_name + build_requires = setupinfo_dict.get("build_requires", ()) + build_backend = setupinfo_dict.get("build_backend", ()) + if not creation_kwargs.get("pyproject_requires") and build_requires: + creation_kwargs["pyproject_requires"] = tuple(build_requires) + if not creation_kwargs.get("pyproject_backend") and build_backend: + creation_kwargs["pyproject_backend"] = build_backend + if setup_info is None and parsed_line and parsed_line.setup_info: + setup_info = parsed_line.setup_info + creation_kwargs["setup_info"] = setup_info if path or relpath: creation_kwargs["path"] = relpath if relpath else path if req is not None: @@ -1384,12 +1400,13 @@ def line_part(self): raise ValueError("Could not calculate url for {0!r}".format(self)) return "{0}{1}".format(editable, seed) + @property def pipfile_part(self): # type: () -> Dict[str, Dict[str, Any]] excludes = [ "_base_line", "_has_hashed_name", "setup_path", "pyproject_path", "_uri_scheme", - "pyproject_requires", "pyproject_backend", "setup_info", "_parsed_line" + "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() @@ -1473,8 +1490,6 @@ def __attrs_post_init__(self): self.parsed_line.ireq and not self.parsed_line.ireq.req ): self.parsed_line._ireq.req = self.req - if self.setup_info is None and self.parsed_line and self.parsed_line.setup_info: - self.setup_info = self.parsed_line.setup_info @link.default def get_link(self): @@ -1834,7 +1849,7 @@ def pipfile_part(self): # type: () -> Dict[str, Dict[str, Union[List[str], str, bool]]] excludes = [ "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", - "pyproject_requires", "pyproject_backend", "setup_info", "_parsed_line" + "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() @@ -2225,7 +2240,7 @@ def get_markers(self): def get_specifier(self): # type: () -> Union[SpecifierSet, LegacySpecifier] try: - return SpecifierSet(self.specifiers) + return Specifier(self.specifiers) except InvalidSpecifier: return LegacySpecifier(self.specifiers) @@ -2275,7 +2290,7 @@ def as_pipfile(self): base_dict = { k: v for k, v in self.req.pipfile_part[name].items() - if k not in ["req", "link", "setup_info"] + if k not in ["req", "link", "_setup_info"] } base_dict.update(req_dict) conflicting_keys = ("file", "path", "uri") @@ -2414,7 +2429,7 @@ def run_requires(self, sources=None, finder=None): return {} info_dict = info.get_info() if self.req and not self.req.setup_info: - self.req.setup_info = info + self.req._setup_info = info if self.req._has_hashed_name and info_dict.get("name"): self.req.name = self.name = info_dict["name"] if self.req.req.name != info_dict["name"]: From fe492ffe802d9aeee71fed7ee8d9f9c1a6046a18 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 13 Feb 2019 18:53:29 -0500 Subject: [PATCH 12/35] Recover files from deletion Signed-off-by: Dan Ryan --- CHANGELOG.rst | 74 +- CONTRIBUTING.rst | 2 +- README.rst | 21 +- src/requirementslib/models/pipfile.py | 63 +- src/requirementslib/models/requirements.py | 1426 ++++++++++++++------ src/requirementslib/models/setup_info.py | 567 ++++++-- src/requirementslib/models/utils.py | 249 +++- src/requirementslib/utils.py | 55 +- 8 files changed, 1781 insertions(+), 676 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a575934c..d39c6f7a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,17 +5,17 @@ Features -------- - Added ``is_pep517`` and ``build_backend`` properties to the top level ``Requirement`` object to help determine how to build the requirement. #125 - + Bug Fixes --------- - Suppressed output written to ``stdout`` by pip during clones of repositories to non-base branches. #124 - + - Fixed a bug which caused local file and VCS requirements to be discovered in a depth-first, inexact search, which sometimes caused incorrect matches to be returned. #128 - + - Fixed a bug with link generation on VCS requirements without URI schemes. #132 - + - ``VCSRequirement.get_checkout_dir`` will now properly respect the ``src_dir`` argument. #133 @@ -35,15 +35,15 @@ Features -------- - Enhanced parsing of dependency and extras detail from ``setup.cfg`` files. #118 - + Bug Fixes --------- - Take the path passed in if it's valid when loading or creating the lockfile/pipfile. #114 - + - Don't write redundant ``egg-info`` under project root when ``src`` is used as package base. #115 - + - Fixed an issue which prevented parsing of extras and dependency information from local ``setup.py`` files and could cause irrecoverable errors. #116 @@ -63,17 +63,17 @@ Features -------- - Added support for loading metadata from ``pyproject.toml``. #102 - + - Local and remote archive ``FileRequirements`` will now be unpacked to a temporary directory for parsing. #103 - + - Dependency information will now be parsed from local paths, including locally unpacked archives, via ``setup.py egg_info`` execution. #104 - + - Additional metadata will now be gathered for ``Requirement`` objects which contain a ``setup.cfg`` on their base path. #105 - + - Requirement names will now be harvested from all available sources, including from ``setup.py`` execution, ``setup.cfg`` files, and any metadata provided as input. #107 - + - Added a flag for PEP508 style direct url requirements. #99 - + Bug Fixes --------- @@ -133,15 +133,15 @@ Features -------- - ``Pipfile`` and ``Lockfile`` models will now properly perform import and export operations with fully data serialization. #83 - + - Added a new interface for merging ``dev`` and ``default`` sections in both ``Pipfile`` and ``Lockfile`` objects using ``get_deps(dev=True, only=False)``. #85 - + Bug Fixes --------- - ``Requirement.as_line()`` now provides an argument to make the inclusion of markers optional by passing ``include_markers=False``. #82 - + - ``Pipfile`` and ``Lockfile`` models are now able to successfully perform creation operations on projects which currently do not have existing files if supplied ``create=True``. #84 @@ -161,7 +161,7 @@ Bug Fixes --------- - Fixed a bug which caused VCS URIs to build incorrectly when calling ``VCSRequirement.as_line()`` in some cases. #73 - + - Fixed bug that editable package with ref by @ is not supported correctly #74 @@ -181,19 +181,19 @@ Features -------- - ``Requirement.get_commit_hash`` and ``Requirement.update_repo`` will no longer clone local repositories to temporary directories or local src directories in order to determine commit hashes. #60 - + - Added ``Requirement.lock_vcs_ref()`` api for locking the VCS commit hash to the current commit (and obtaining it and determining it if necessary). #64 - + - ``Requirement.as_line()`` now offers the parameter ``as_list`` to return requirements more suited for passing directly to ``subprocess.run`` and ``subprocess.Popen`` calls. #67 - + Bug Fixes --------- - Fixed a bug error formatting of the path validator method of local requirements. #57 - + - Fixed an issue which prevented successful loads of ``Pipfile`` objects missing entries in some sections. #59 - + - Fixed an issue which caused ``Requirement.get_commit_hash()`` to fail for local requirements. #67 @@ -240,7 +240,7 @@ Bug Fixes --------- - Fixed a bug which sometimes caused extras to be dropped when parsing named requirements using constraint-style specifiers. #44 - + - Fix parsing error in `Requirement.as_ireq()` if requirement contains hashes. #45 @@ -251,37 +251,37 @@ Features -------- - Added support for ``Requirement.get_dependencies()`` to return unpinned dependencies. - Implemented full support for both parsing and writing lockfiles. - Introduced lazy imports to enhance runtime performance. - Switch to ``packaging.canonicalize_name()`` instead of custom canonicalization function. - Added ``Requirement.copy()`` to the api to copy a requirement. #33 - +- Implemented full support for both parsing and writing lockfiles. +- Introduced lazy imports to enhance runtime performance. +- Switch to ``packaging.canonicalize_name()`` instead of custom canonicalization function. +- Added ``Requirement.copy()`` to the api to copy a requirement. #33 + - Add pep423 formatting to package names when generating ``as_line()`` output. - Sort extras when building lines. - Improve local editable requirement name resolution. #36 - +- Sort extras when building lines. +- Improve local editable requirement name resolution. #36 + Bug Fixes --------- -- - Fixed a bug which prevented dependency resolution using pip >= 18.0. +- Fixed a bug which prevented dependency resolution using pip >= 18.0. + +- Fix pipfile parser bug which mishandled missing ``requires`` section. #33 - - Fix pipfile parser bug which mishandled missing ``requires`` section. #33 - - Fixed a bug which caused extras to be excluded from VCS urls generated from pipfiles. #41 - + Vendored Libraries ------------------ - Unvendored ``pipfile`` in favor of ``plette``. #33 - + Removals and Deprecations ------------------------- - Unvendored ``pipfile`` in favor of ``plette``. #33 - + - Moved pipfile and lockfile models to ``plette`` and added api wrappers for compatibility. #43 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 927e09fc..2633dd33 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,5 +1,5 @@ Contributing to requirementslib -================================ +==================================== To work on requirementslib itself, fork the repository and clone your fork to your local system. diff --git a/README.rst b/README.rst index 0d20d445..b0379469 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,4 @@ RequirementsLib: Requirement Management Library for Pip and Pipenv -=========================================================================== .. image:: https://img.shields.io/pypi/v/requirementslib.svg :target: https://pypi.org/project/requirementslib @@ -201,3 +200,23 @@ requirement itself via the property ``requirement.req.dependencies``: * `Pip `_ * `Pipenv `_ * `Pipfile`_ +======= +Copyright 2016 Kenneth Reitz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/requirementslib/models/pipfile.py b/src/requirementslib/models/pipfile.py index 84a4a26d..021b5d53 100644 --- a/src/requirementslib/models/pipfile.py +++ b/src/requirementslib/models/pipfile.py @@ -18,16 +18,16 @@ from ..utils import is_editable, is_vcs, merge_items from .project import ProjectFile from .requirements import Requirement -from .utils import optional_instance_of +from .utils import optional_instance_of, get_url_name from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Union, Any, Dict, Iterable, Sequence, Mapping, List, NoReturn - package_type = Dict[str, Dict[str, Union[List[str], str]]] - source_type = Dict[str, Union[str, bool]] + from typing import Union, Any, Dict, Iterable, Sequence, Mapping, List, NoReturn, Text + package_type = Dict[Text, Dict[Text, Union[List[Text], Text]]] + source_type = Dict[Text, Union[Text, bool]] sources_type = Iterable[source_type] - meta_type = Dict[str, Union[int, Dict[str, str], sources_type]] - lockfile_type = Dict[str, Union[package_type, meta_type]] + meta_type = Dict[Text, Union[int, Dict[Text, Text], sources_type]] + lockfile_type = Dict[Text, Union[package_type, meta_type]] # Let's start by patching plette to make sure we can validate data without being broken @@ -45,7 +45,7 @@ def patch_plette(): global VALIDATORS def validate(cls, data): - # type: (Any, Dict[str, Any]) -> None + # type: (Any, Dict[Text, Any]) -> None if not cerberus: # Skip validation if Cerberus is not available. return schema = cls.__SCHEMA__ @@ -87,9 +87,10 @@ def reorder_source_keys(data): sources = data["source"] # type: sources_type for i, entry in enumerate(sources): table = tomlkit.table() # type: Mapping - table["name"] = entry["name"] - table["url"] = entry["url"] - table["verify_ssl"] = entry["verify_ssl"] + source_entry = PipfileLoader.populate_source(entry.copy()) + table["name"] = source_entry["name"] + table["url"] = source_entry["url"] + table["verify_ssl"] = source_entry["verify_ssl"] data["source"][i] = table return data @@ -97,7 +98,7 @@ def reorder_source_keys(data): class PipfileLoader(plette.pipfiles.Pipfile): @classmethod def validate(cls, data): - # type: (Dict[str, Any]) -> None + # type: (Dict[Text, Any]) -> None for key, klass in plette.pipfiles.PIPFILE_SECTIONS.items(): if key not in data or key == "source": continue @@ -106,9 +107,21 @@ def validate(cls, data): except Exception: pass + @classmethod + def populate_source(cls, source): + """Derive missing values of source from the existing fields.""" + # Only URL pararemter is mandatory, let the KeyError be thrown. + if "name" not in source: + source["name"] = get_url_name(source["url"]) + if "verify_ssl" not in source: + source["verify_ssl"] = "https://" in source["url"] + if not isinstance(source["verify_ssl"], bool): + source["verify_ssl"] = source["verify_ssl"].lower() == "true" + return source + @classmethod def load(cls, f, encoding=None): - # type: (Any, str) -> PipfileLoader + # type: (Any, Text) -> PipfileLoader content = f.read() if encoding is not None: content = content.decode(encoding) @@ -132,7 +145,7 @@ def load(cls, f, encoding=None): return instance def __getattribute__(self, key): - # type: (str) -> Any + # type: (Text) -> Any if key == "source": return self._data[key] return super(PipfileLoader, self).__getattribute__(key) @@ -169,8 +182,8 @@ def pipfile(self): return self._pipfile def get_deps(self, dev=False, only=True): - # type: (bool, bool) -> Dict[str, Dict[str, Union[List[str], str]]] - deps = {} # type: Dict[str, Dict[str, Union[List[str], str]]] + # type: (bool, bool) -> Dict[Text, Dict[Text, Union[List[Text], Text]]] + deps = {} # type: Dict[Text, Dict[Text, Union[List[Text], Text]]] if dev: deps.update(self.pipfile._data["dev-packages"]) if only: @@ -178,11 +191,11 @@ def get_deps(self, dev=False, only=True): return merge_items([deps, self.pipfile._data["packages"]]) def get(self, k): - # type: (str) -> Any + # type: (Text) -> Any return self.__getitem__(k) def __contains__(self, k): - # type: (str) -> bool + # type: (Text) -> bool check_pipfile = k in self.extended_keys or self.pipfile.__contains__(k) if check_pipfile: return True @@ -234,10 +247,10 @@ def allow_prereleases(self): @classmethod def read_projectfile(cls, path): - # type: (str) -> ProjectFile + # type: (Text) -> ProjectFile """Read the specified project file and provide an interface for writing/updating. - :param str path: Path to the target file. + :param Text path: Path to the target file. :return: A project file with the model and location for interaction :rtype: :class:`~requirementslib.models.project.ProjectFile` """ @@ -250,10 +263,10 @@ def read_projectfile(cls, path): @classmethod def load_projectfile(cls, path, create=False): - # type: (str, bool) -> ProjectFile + # type: (Text, bool) -> ProjectFile """Given a path, load or create the necessary pipfile. - :param str path: Path to the project root or pipfile + :param Text path: Path to the project root or pipfile :param bool create: Whether to create the pipfile if not found, defaults to True :raises OSError: Thrown if the project root directory doesn't exist :raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False`` @@ -275,10 +288,10 @@ def load_projectfile(cls, path, create=False): @classmethod def load(cls, path, create=False): - # type: (str, bool) -> Pipfile + # type: (Text, bool) -> Pipfile """Given a path, load or create the necessary pipfile. - :param str path: Path to the project root or pipfile + :param Text path: Path to the project root or pipfile :param bool create: Whether to create the pipfile if not found, defaults to True :raises OSError: Thrown if the project root directory doesn't exist :raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False`` @@ -334,10 +347,10 @@ def _read_pyproject(self): @property def build_requires(self): - # type: () -> List[str] + # type: () -> List[Text] return self.build_system.get("requires", []) @property def build_backend(self): - # type: () -> str + # type: () -> Text return self.build_system.get("build-backend", None) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index ef3e3c36..90472d12 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -6,7 +6,11 @@ import copy import hashlib import os +import re +import string +import sys +from distutils.sysconfig import get_python_lib from contextlib import contextmanager from functools import partial @@ -14,6 +18,7 @@ import pep517 import pep517.wrappers import pip_shims +import six import vistir from first import first @@ -23,7 +28,8 @@ from packaging.utils import canonicalize_name from six.moves.urllib import parse as urllib_parse from six.moves.urllib.parse import unquote -from vistir.compat import Path +from vistir.compat import Path, Iterable, FileNotFoundError +from vistir.contextmanagers import temp_path from vistir.misc import dedup from vistir.path import ( create_tracked_tempdir, @@ -58,6 +64,7 @@ parse_extras, specs_to_string, split_markers_from_line, + split_ref_from_uri, split_vcs_method_from_uri, validate_path, validate_specifiers, @@ -65,16 +72,21 @@ normalize_name, create_link, get_pyproject, + convert_direct_url_to_url, + convert_url_to_direct_url, + URL_RE, + DIRECT_URL_RE ) from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, NoReturn + from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Generator, Set, Text from pip_shims.shims import Link, InstallRequirement RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) from six.moves.urllib.parse import SplitResult from .vcs import VCSRepository + NON_STRING_ITERABLE = Union[List, Set, Tuple] SPECIFIERS_BY_LENGTH = sorted(list(Specifier._operators.keys()), key=len, reverse=True) @@ -84,39 +96,42 @@ class Line(object): - def __init__(self, line): - # type: (str) -> None - self.editable = line.startswith("-e ") - if self.editable: + def __init__(self, line, extras=None): + # type: (Text, Optional[NON_STRING_ITERABLE]) -> None + self.editable = False # type: bool + if line.startswith("-e "): line = line[len("-e "):] - self.line = line - self.hashes = [] # type: List[str] - self.extras = [] # type: List[str] - self.markers = None # type: Optional[str] - self.vcs = None # type: Optional[str] - self.path = None # type: Optional[str] - self.relpath = None # type: Optional[str] - self.uri = None # type: Optional[str] + self.editable = True + self.extras = () # type: Tuple[Text] + if extras is not None: + self.extras = tuple(sorted(set(extras))) + self.line = line # type: Text + self.hashes = [] # type: List[Text] + self.markers = None # type: Optional[Text] + self.vcs = None # type: Optional[Text] + self.path = None # type: Optional[Text] + self.relpath = None # type: Optional[Text] + self.uri = None # type: Optional[Text] self._link = None # type: Optional[Link] - self.is_local = False - self.name = None # type: Optional[str] - self.specifier = None # type: Optional[str] + self.is_local = False # type: bool + self._name = None # type: Optional[Text] + self._specifier = None # type: Optional[Text] self.parsed_marker = None # type: Optional[Marker] - self.preferred_scheme = None # type: Optional[str] - self.requirement = None # type: Optional[PackagingRequirement] + self.preferred_scheme = None # type: Optional[Text] + self._requirement = None # type: Optional[PackagingRequirement] self.is_direct_url = False # type: bool self._parsed_url = None # type: Optional[urllib_parse.ParseResult] - self._setup_cfg = None # type: Optional[str] - self._setup_py = None # type: Optional[str] - self._pyproject_toml = None # type: Optional[str] - self._pyproject_requires = None # type: Optional[List[str]] - self._pyproject_backend = None # type: Optional[str] - self._wheel_kwargs = None # type: Dict[str, str] + self._setup_cfg = None # type: Optional[Text] + self._setup_py = None # type: Optional[Text] + self._pyproject_toml = None # type: Optional[Text] + self._pyproject_requires = None # type: Optional[List[Text]] + self._pyproject_backend = None # type: Optional[Text] + self._wheel_kwargs = None # type: Dict[Text, Text] self._vcsrepo = None # type: Optional[VCSRepository] self._setup_info = None # type: Optional[SetupInfo] - self._ref = None # type: Optional[str] + self._ref = None # type: Optional[Text] self._ireq = None # type: Optional[InstallRequirement] - self._src_root = None # type: Optional[str] + self._src_root = None # type: Optional[Text] self.dist = None # type: Any super(Line, self).__init__() self.parse() @@ -127,14 +142,27 @@ def __hash__(self): tuple(self.hashes), self.vcs, self.ireq) ) + def __repr__(self): + try: + return ( + "".format( + self=self + )) + except Exception: + return "".format(self.__dict__.values()) + @classmethod def split_hashes(cls, line): - # type: (str) -> Tuple[str, List[str]] + # type: (Text) -> Tuple[Text, List[Text]] if "--hash" not in line: return line, [] split_line = line.split() - line_parts = [] # type: List[str] - hashes = [] # type: List[str] + line_parts = [] # type: List[Text] + hashes = [] # type: List[Text] for part in split_line: if part.startswith("--hash"): param, _, value = part.partition("=") @@ -146,17 +174,66 @@ def split_hashes(cls, line): @property def line_with_prefix(self): - # type: () -> str + # type: () -> Text line = self.line + extras_str = extras_to_string(self.extras) if self.is_direct_url: line = self.link.url + # if self.link.egg_info and self.extras: + # line = "{0}{1}".format(line, extras_str) + elif extras_str: + if self.is_vcs: + line = self.link.url + if "git+file:/" in line and "git+file:///" not in line: + line = line.replace("git+file:/", "git+file:///") + else: + line = "{0}{1}".format(line, extras_str) if self.editable: return "-e {0}".format(line) return line + @property + def line_for_ireq(self): + # type: () -> Text + line = "" + if self.is_file or self.is_url and not self.is_vcs: + scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" + local_line = next(iter([ + os.path.dirname(os.path.abspath(f)) for f in [ + self.setup_py, self.setup_cfg, self.pyproject_toml + ] if f is not None + ]), None) + if local_line and self.extras: + local_line = "{0}{1}".format(local_line, extras_to_string(self.extras)) + line = local_line if local_line is not None else self.line + if scheme == "path": + if not line and self.base_path is not None: + line = os.path.abspath(self.base_path) + else: + if DIRECT_URL_RE.match(self.line): + self._requirement = init_requirement(self.line) + line = convert_direct_url_to_url(self.line) + + if self.editable: + if not line: + if self.is_path or self.is_file: + if not self.path: + line = pip_shims.shims.url_to_path(self.url) + else: + line = self.path + if self.extras: + line = "{0}{1}".format(line, extras_to_string(self.extras)) + else: + line = self.link.url + elif self.is_vcs and not self.editable: + line = add_ssh_scheme_to_git_uri(self.line) + if not line: + line = self.line + return line + @property def base_path(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if not self.link and not self.path: self.parse_link() if not self.path: @@ -172,28 +249,66 @@ def base_path(self): @property def setup_py(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self._setup_py is None: self.populate_setup_paths() return self._setup_py @property def setup_cfg(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self._setup_cfg is None: self.populate_setup_paths() return self._setup_cfg @property def pyproject_toml(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self._pyproject_toml is None: self.populate_setup_paths() return self._pyproject_toml + @property + def specifier(self): + # type: () -> Optional[Text] + options = [self._specifier] + for req in (self.ireq, self.requirement): + if req is not None and getattr(req, "specifier", None): + options.append(req.specifier) + specifier = next(iter(spec for spec in options if spec is not None), None) + if specifier is not None: + specifier = specs_to_string(specifier) + elif specifier is None and not self.is_named and self._setup_info is not None: + if self._setup_info.version: + specifier = "=={0}".format(self._setup_info.version) + if specifier: + self._specifier = specifier + return self._specifier + + @specifier.setter + def specifier(self, spec): + # type: (str) -> None + if not spec.startswith("=="): + spec = "=={0}".format(spec) + self._specifier = spec + self.specifiers = SpecifierSet(spec) + @property def specifiers(self): # type: () -> Optional[SpecifierSet] + ireq_needs_specifier = False + req_needs_specifier = False + if self.ireq is None or self.ireq.req is None or not self.ireq.req.specifier: + ireq_needs_specifier = True + if self.requirement is None or not self.requirement.specifier: + req_needs_specifier = True + if any([ireq_needs_specifier, req_needs_specifier]): + # TODO: Should we include versions for VCS dependencies? IS there a reason not + # to? For now we are using hashes as the equivalent to pin + # note: we need versions for direct dependencies at the very least + if self.is_file or self.is_url or self.is_path or (self.is_vcs and not self.editable): + if self.specifier is not None: + self.specifiers = self.specifier if self.ireq is not None and self.ireq.req is not None: return self.ireq.req.specifier elif self.requirement is not None: @@ -202,16 +317,42 @@ def specifiers(self): @specifiers.setter def specifiers(self, specifiers): - # type: (Union[str, SpecifierSet]) -> None + # type: (Union[Text, SpecifierSet]) -> None if type(specifiers) is not SpecifierSet: if type(specifiers) in six.string_types: specifiers = SpecifierSet(specifiers) else: raise TypeError("Must pass a string or a SpecifierSet") + specs = self.get_requirement_specs(specifiers) if self.ireq is not None and self.ireq.req is not None: self._ireq.req.specifier = specifiers + self._ireq.req.specs = specs if self.requirement is not None: self.requirement.specifier = specifiers + self.requirement.specs = specs + + @classmethod + def get_requirement_specs(cls, specifierset): + # type: (SpecifierSet) -> List[Tuple[Text, Text]] + specs = [] + spec = next(iter(specifierset._specs), None) + if spec: + specs.append(spec._spec) + return specs + + @property + def requirement(self): + # type: () -> Optional[PackagingRequirement] + if self._requirement is None: + self.parse_requirement() + if self._requirement is None and self._name is not None: + self._requirement = init_requirement(canonicalize_name(self.name)) + if self.is_file or self.is_url and self._requirement is not None: + self._requirement.url = self.url + if self._requirement and self._requirement.specifier and not self._requirement.specs: + specs = self.get_requirement_specs(self._requirement.specifier) + self._requirement.specs = specs + return self._requirement def populate_setup_paths(self): # type: () -> None @@ -222,14 +363,14 @@ def populate_setup_paths(self): base_path = self.base_path if base_path is None: return - setup_paths = get_setup_paths(self.base_path, subdirectory=self.subdirectory) # type: Dict[str, Optional[str]] + setup_paths = get_setup_paths(self.base_path, subdirectory=self.subdirectory) # type: Dict[Text, Optional[Text]] self._setup_py = setup_paths.get("setup_py") self._setup_cfg = setup_paths.get("setup_cfg") self._pyproject_toml = setup_paths.get("pyproject_toml") @property def pyproject_requires(self): - # type: () -> Optional[List[str]] + # type: () -> Optional[List[Text]] if self._pyproject_requires is None and self.pyproject_toml is not None: pyproject_requires, pyproject_backend = get_pyproject(self.path) self._pyproject_requires = pyproject_requires @@ -238,12 +379,12 @@ def pyproject_requires(self): @property def pyproject_backend(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self._pyproject_requires is None and self.pyproject_toml is not None: pyproject_requires, pyproject_backend = get_pyproject(self.path) if not pyproject_backend and self.setup_cfg is not None: setup_dict = SetupInfo.get_setup_cfg(self.setup_cfg) - pyproject_backend = "setuptools.build_meta" + pyproject_backend = "setuptools.build_meta:__legacy__" pyproject_requires = setup_dict.get("build_requires", ["setuptools", "wheel"]) self._pyproject_requires = pyproject_requires @@ -271,57 +412,148 @@ def parse_extras(self): """ extras = None - if "@" in self.line: - parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(self.line)) - if not parsed.scheme: - name, _, line = self.line.partition("@") - name = name.strip() - line = line.strip() - if is_vcs(line) or is_valid_url(line): - self.is_direct_url = True - name, extras = pip_shims.shims._strip_extras(name) - self.name = name - self.line = line + if "@" in self.line or self.is_vcs or self.is_url: + line = "{0}".format(self.line) + match = DIRECT_URL_RE.match(line) + if match is None: + match = URL_RE.match(line) + else: + self.is_direct_url = True + if match is not None: + match_dict = match.groupdict() + name = match_dict.get("name") + extras = match_dict.get("extras") + scheme = match_dict.get("scheme") + host = match_dict.get("host") + path = match_dict.get("path") + ref = match_dict.get("ref") + subdir = match_dict.get("subdirectory") + pathsep = match_dict.get("pathsep", "/") + url = scheme + if host: + url = "{0}{1}".format(url, host) + if path: + url = "{0}{1}{2}".format(url, pathsep, path) + if self.is_vcs and ref: + url = "{0}@{1}".format(url, ref) + if name: + url = "{0}#egg={1}".format(url, name) + if extras: + url = "{0}{1}".format(url, extras) + elif is_file_url(url) and extras and not name and self.editable: + url = "{0}{1}{2}".format(pathsep, path, extras) + if subdir: + url = "{0}&subdirectory={1}".format(url, subdir) + elif extras and not path: + url = "{0}{1}".format(url, extras) + self.line = add_ssh_scheme_to_git_uri(url) + if name: + self._name = name + # line = add_ssh_scheme_to_git_uri(self.line) + # parsed = urllib_parse.urlparse(line) + # if not parsed.scheme and "@" in line: + # matched = URL_RE.match(line) + # if matched is None: + # matched = NAME_RE.match(line) + # if matched: + # name = matched.groupdict().get("name") + # if name is not None: + # self._name = name + # extras = matched.groupdict().get("extras") + # else: + # name, _, line = self.line.partition("@") + # name = name.strip() + # line = line.strip() + # matched = NAME_RE.match(name) + # match_dict = matched.groupdict() + # name = match_dict.get("name") + # extras = match_dict.get("extras") + # if is_vcs(line) or is_valid_url(line): + # self.is_direct_url = True + # # name, extras = pip_shims.shims._strip_extras(name) + # self._name = name + # self.line = line else: self.line, extras = pip_shims.shims._strip_extras(self.line) else: self.line, extras = pip_shims.shims._strip_extras(self.line) if extras is not None: - self.extras = parse_extras(extras) + extras = set(parse_extras(extras)) + if self._name: + self._name, name_extras = pip_shims.shims._strip_extras(self._name) + if name_extras: + name_extras = set(parse_extras(name_extras)) + if extras: + extras |= name_extras + else: + extras = name_extras + if extras is not None: + self.extras = tuple(sorted(extras)) def get_url(self): - # type: () -> str + # type: () -> Text """Sets ``self.name`` if given a **PEP-508** style URL""" line = self.line if self.vcs is not None and self.line.startswith("{0}+".format(self.vcs)): _, _, _parseable = self.line.partition("+") parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(_parseable)) + line, _ = split_ref_from_uri(line) else: parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(line)) if "@" in self.line and parsed.scheme == "": name, _, url = self.line.partition("@") - if self.name is None: - self.name = name + if self._name is None: + url = url.strip() + self._name = name.strip() if is_valid_url(url): self.is_direct_url = True line = url.strip() parsed = urllib_parse.urlparse(line) + url_path = parsed.path + if "@" in url_path: + url_path, _, _ = url_path.rpartition("@") + parsed = parsed._replace(path=url_path) self._parsed_url = parsed return line + @property + def name(self): + # type: () -> Optional[Text] + if self._name is None: + self.parse_name() + if self._name is None and not self.is_named and not self.is_wheel: + if self.setup_info: + self._name = self.setup_info.name + return self._name + + @name.setter + def name(self, name): + # type: (Text) -> None + self._name = name + if self._setup_info: + self._setup_info.name = name + if self.requirement: + self._requirement.name = name + if self.ireq and self.ireq.req: + self._ireq.req.name = name + @property def url(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self.uri is not None: url = add_ssh_scheme_to_git_uri(self.uri) - url = getattr(self.link, "url_without_fragment", None) + else: + url = getattr(self.link, "url_without_fragment", None) if url is not None: url = add_ssh_scheme_to_git_uri(unquote(url)) if url is not None and self._parsed_url is None: if self.vcs is not None: _, _, _parseable = url.partition("+") self._parsed_url = urllib_parse.urlparse(_parseable) + if self.is_vcs: + # strip the ref from the url + url, _ = split_ref_from_uri(url) return url @property @@ -333,7 +565,7 @@ def link(self): @property def subdirectory(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self.link is not None: return self.link.subdirectory_fragment return "" @@ -375,30 +607,45 @@ def is_path(self): if self.path and ( self.path.startswith(".") or os.path.isabs(self.path) or os.path.exists(self.path) + ) and is_installable_file(self.path): + return True + elif (os.path.exists(self.line) and is_installable_file(self.line)) or ( + os.path.exists(self.get_url()) and is_installable_file(self.get_url()) ): return True - elif os.path.exists(self.line) or os.path.exists(self.get_url()): + return False + + @property + def is_file_url(self): + # type: () -> bool + url = self.get_url() + parsed_url_scheme = self._parsed_url.scheme if self._parsed_url else "" + if url and is_file_url(self.get_url()) or parsed_url_scheme == "file": return True return False @property def is_file(self): # type: () -> bool - if self.is_path or is_file_url(self.get_url()) or (self._parsed_url and self._parsed_url.scheme == "file"): + if self.is_path or ( + is_file_url(self.get_url()) and is_installable_file(self.get_url()) + ) or ( + self._parsed_url and self._parsed_url.scheme == "file" and + is_installable_file(urllib_parse.urlunparse(self._parsed_url)) + ): return True return False @property def is_named(self): # type: () -> bool - return not (self.is_file or self.is_url or self.is_vcs) + return not (self.is_file_url or self.is_url or self.is_file or self.is_vcs) @property def ref(self): - # type: () -> Optional[str] - if self._ref is None: - if self.relpath and "@" in self.relpath: - self._relpath, _, self._ref = self.relpath.rpartition("@") + # type: () -> Optional[Text] + if self._ref is None and self.relpath is not None: + self.relpath, self._ref = split_ref_from_uri(self.relpath) return self._ref @property @@ -411,19 +658,49 @@ def ireq(self): @property def is_installable(self): # type: () -> bool - if is_installable_file(self.line) or is_installable_file(self.get_url()) or is_installable_file(self.path) or is_installable_file(self.base_path): - return True - return False + possible_paths = (self.line, self.get_url(), self.path, self.base_path) + return any(is_installable_file(p) for p in possible_paths if p is not None) + + @property + def wheel_kwargs(self): + if not self._wheel_kwargs: + self._wheel_kwargs = _prepare_wheel_building_kwargs(self.ireq) + return self._wheel_kwargs + + def get_setup_info(self): + # type: () -> SetupInfo + setup_info = SetupInfo.from_ireq(self.ireq) + if not setup_info.name: + setup_info.get_info() + return setup_info @property def setup_info(self): # type: () -> Optional[SetupInfo] - if self._setup_info is None and not self.is_named: - self._setup_info = SetupInfo.from_ireq(self.ireq) - if self._setup_info is not None: - self._setup_info.get_info() + if self._setup_info is None and not self.is_named and not self.is_wheel: + if self._setup_info: + if not self._setup_info.name: + self._setup_info.get_info() + else: + # make two attempts at this before failing to allow for stale data + try: + self.setup_info = self.get_setup_info() + except FileNotFoundError: + try: + self.setup_info = self.get_setup_info() + except FileNotFoundError: + raise return self._setup_info + @setup_info.setter + def setup_info(self, setup_info): + # type: (SetupInfo) -> None + self._setup_info = setup_info + if setup_info.version: + self.specifier = setup_info.version + if setup_info.name and not self.name: + self.name = setup_info.name + def _get_vcsrepo(self): # type: () -> Optional[VCSRepository] from .vcs import VCSRepository @@ -438,58 +715,56 @@ def _get_vcsrepo(self): vcs_type=self.vcs, subdirectory=self.subdirectory, ) - if not self.link.scheme.startswith("file"): + if not ( + self.link.scheme.startswith("file") and + self.editable + ): vcsrepo.obtain() return vcsrepo @property def vcsrepo(self): # type: () -> Optional[VCSRepository] - if self._vcsrepo is None: + if self._vcsrepo is None and self.is_vcs: self._vcsrepo = self._get_vcsrepo() return self._vcsrepo + @vcsrepo.setter + def vcsrepo(self, repo): + # type (VCSRepository) -> None + self._vcsrepo = repo + ireq = self.ireq + wheel_kwargs = self.wheel_kwargs.copy() + wheel_kwargs["src_dir"] = repo.checkout_directory + ireq.source_dir = wheel_kwargs["src_dir"] + build_dir = ireq.build_location(wheel_kwargs["build_dir"]) + ireq._temp_build_dir.path = wheel_kwargs["build_dir"] + with temp_path(): + sys.path = [repo.checkout_directory, "", ".", get_python_lib(plat_specific=0)] + setupinfo = SetupInfo.create( + repo.checkout_directory, ireq=ireq, subdirectory=self.subdirectory, + kwargs=wheel_kwargs + ) + self._setup_info = setupinfo + self._setup_info.reload() + def get_ireq(self): # type: () -> InstallRequirement + line = self.line_for_ireq + if self.editable: + ireq = pip_shims.shims.install_req_from_editable(line) + else: + ireq = pip_shims.shims.install_req_from_line(line) if self.is_named: ireq = pip_shims.shims.install_req_from_line(self.line) - elif (self.is_file or self.is_url) and not self.is_vcs: - line = self.line - if self.is_direct_url: - line = self.link.url - scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" - local_line = next(iter([ - os.path.dirname(os.path.abspath(f)) for f in [ - self.setup_py, self.setup_cfg, self.pyproject_toml - ] if f is not None - ]), None) - line = local_line if local_line is not None else self.line - if scheme == "path": - if not line and self.base_path is not None: - line = os.path.abspath(self.base_path) - else: - if self.link is not None: - line = self.link.url_without_fragment - else: - if self.uri is not None: - line = self.uri - else: - line = self.path - if self.editable: - ireq = pip_shims.shims.install_req_from_editable(self.link.url) - else: - ireq = pip_shims.shims.install_req_from_line(line) - else: - if self.editable: - ireq = pip_shims.shims.install_req_from_editable(self.link.url) - else: - ireq = pip_shims.shims.install_req_from_line(self.link.url) + if self.is_file or self.is_url: + ireq.link = self.link if self.extras and not ireq.extras: ireq.extras = set(self.extras) if self.parsed_marker is not None and not ireq.markers: ireq.markers = self.parsed_marker - if not ireq.req and self.requirement is not None: - ireq.req = PackagingRequirement(str(self.requirement)) + if not ireq.req and self._requirement is not None: + ireq.req = copy.deepcopy(self._requirement) return ireq def parse_ireq(self): @@ -501,18 +776,18 @@ def parse_ireq(self): self._ireq.req = self.requirement def _parse_wheel(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if not self.is_wheel: pass from pip_shims.shims import Wheel _wheel = Wheel(self.link.filename) name = _wheel.name version = _wheel.version - self.specifier = "=={0}".format(version) + self._specifier = "=={0}".format(version) return name def _parse_name_from_link(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self.link is None: return None @@ -523,22 +798,32 @@ def _parse_name_from_link(self): return None def _parse_name_from_line(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if not self.is_named: pass - name = self.line - specifier_match = next( - iter(spec for spec in SPECIFIERS_BY_LENGTH if spec in self.line), None - ) - if specifier_match is not None: - name, specifier_match, version = name.partition(specifier_match) - self.specifier = "{0}{1}".format(specifier_match, version) + try: + self._requirement = init_requirement(self.line) + except Exception: + raise RequirementError("Failed parsing requirement from {0!r}".format(self.line)) + name = self._requirement.name + if not self._specifier and self._requirement and self._requirement.specifier: + self._specifier = specs_to_string(self._requirement.specifier) + if self._requirement.extras and not self.extras: + self.extras = self._requirement.extras + if not name: + name = self.line + specifier_match = next( + iter(spec for spec in SPECIFIERS_BY_LENGTH if spec in self.line), None + ) + if specifier_match is not None: + name, specifier_match, version = name.partition(specifier_match) + self._specifier = "{0}{1}".format(specifier_match, version) return name def parse_name(self): # type: () -> None - if self.name is None: + if self._name is None: name = None if self.link is not None: name = self._parse_name_from_link() @@ -550,97 +835,118 @@ def parse_name(self): if "&" in name: # subdirectory fragments might also be in here name, _, _ = name.partition("&") - if self.is_named and name is None: + if self.is_named: name = self._parse_name_from_line() if name is not None: name, extras = pip_shims.shims._strip_extras(name) if extras is not None and not self.extras: - self.extras = parse_extras(extras) - self.name = name + self.extras = tuple(sorted(set(parse_extras(extras)))) + self._name = name def _parse_requirement_from_vcs(self): # type: () -> Optional[PackagingRequirement] - name = self.name if self.name else self.link.egg_fragment - url = self.uri if self.uri else unquote(self.link.url) - if self.is_direct_url: - url = self.link.url - if not name: - raise ValueError( - "pipenv requires an #egg fragment for version controlled " - "dependencies. Please install remote dependency " - "in the form {0}#egg=.".format(url) - ) - req = init_requirement(canonicalize_name(name)) # type: PackagingRequirement - req.editable = self.editable - if not getattr(req, "url") and self.link: - req.url = url - req.line = self.link.url if ( - self.uri != unquote(self.link.url_without_fragment) - and "git+ssh://" in self.link.url + self.uri != unquote(self.url) + and "git+ssh://" in self.url and (self.uri is not None and "git+git@" in self.uri) ): - req.line = self.uri - req.url = self.uri + self._requirement.line = self.uri + self._requirement.url = self.url + self._requirement.link = create_link(build_vcs_uri( + vcs=self.vcs, + uri=self.url, + ref=self.ref, + subdirectory=self.subdirectory, + extras=self.extras, + name=self.name + )) + # else: + # req.link = self.link if self.ref: if self._vcsrepo is not None: - req.revision = self._vcsrepo.get_commit_hash() + self._requirement.revision = self._vcsrepo.get_commit_hash() else: - req.revision = self.ref - if self.extras: - req.extras = self.extras - req.vcs = self.vcs - req.link = self.link - if self.path and self.link and self.link.scheme.startswith("file"): - req.local_file = True - req.path = self.path - return req + self._requirement.revision = self.ref + return self._requirement def parse_requirement(self): # type: () -> None - if self.name is None: + if self._name is None: self.parse_name() - if self.is_named: - self.requirement = init_requirement(self.line) - elif self.is_vcs: - self.requirement = self._parse_requirement_from_vcs() - if self.name is None and ( - self.requirement is not None and self.requirement.name is not None - ): - self.name = self.requirement.name - if self.name is not None and self.requirement is None: - self.requirement = init_requirement(self.name) - if self.requirement: + if not self._name and not self.is_vcs and not self.is_named: + if self.setup_info and self.setup_info.name: + self._name = self.setup_info.name + name, extras, url = self.requirement_info + if name: + self._requirement = init_requirement(name) # type: PackagingRequirement + if extras: + self._requirement.extras = set(extras) + if url: + self._requirement.url = url + if self.is_direct_url: + url = self.link.url + if self.link: + self._requirement.link = self.link + self._requirement.editable = self.editable + if self.path and self.link and self.link.scheme.startswith("file"): + self._requirement.local_file = True + self._requirement.path = self.path + if self.is_vcs: + self._requirement.vcs = self.vcs + self._requirement.line = self.link.url + self._parse_requirement_from_vcs() + else: + self._requirement.line = self.line if self.parsed_marker is not None: - self.requirement.marker = self.parsed_marker - if self.is_url or self.is_file and (self.link or self.url) and not self.is_vcs: - if self.uri: - self.requirement.url = self.uri - elif self.link: - self.requirement.url = unquote(self.link.url_without_fragment) - else: - self.requirement.url = self.url - if self.extras and not self.requirement.extras: - self.requirement.extras = set(self.extras) + self._requirement.marker = self.parsed_marker + if self.specifiers: + self._requirement.specifier = self.specifiers + specs = [] + spec = next(iter(s for s in self.specifiers._specs), None) + if spec: + specs.append(spec._spec) + self._requirement.spec = spec + else: + if self.is_vcs: + raise ValueError( + "pipenv requires an #egg fragment for version controlled " + "dependencies. Please install remote dependency " + "in the form {0}#egg=.".format(url) + ) def parse_link(self): # type: () -> None if self.is_file or self.is_url or self.is_vcs: vcs, prefer, relpath, path, uri, link = FileRequirement.get_link_from_line(self.line) ref = None - if link is not None and "@" in link.path and uri is not None: - uri, _, ref = uri.rpartition("@") + if link is not None and "@" in unquote(link.path) and uri is not None: + uri, _, ref = unquote(uri).rpartition("@") if relpath is not None and "@" in relpath: relpath, _, ref = relpath.rpartition("@") + if path is not None and "@" in path: + path, _ = split_ref_from_uri(path) + link_url = link.url_without_fragment + if "@" in link_url: + link_url, _ = split_ref_from_uri(link_url) self._ref = ref self.vcs = vcs self.preferred_scheme = prefer self.relpath = relpath self.path = path self.uri = uri - if self.is_direct_url and self.name is not None and vcs is not None: + if link.egg_fragment: + name, extras = pip_shims.shims._strip_extras(link.egg_fragment) + self.extras = tuple(sorted(set(parse_extras(extras)))) + self._name = name + else: + # set this so we can call `self.name` without a recursion error + self._link = link + if (self.is_direct_url or vcs) and self.name is not None and vcs is not None: self._link = create_link( - build_vcs_uri(vcs=vcs, uri=uri, ref=ref, extras=self.extras, name=self.name) + build_vcs_uri(vcs=vcs, uri=link_url, ref=ref, + extras=self.extras, name=self.name, + subdirectory=link.subdirectory_fragment + ) ) else: self._link = link @@ -651,6 +957,72 @@ def parse_markers(self): markers = PackagingRequirement("fakepkg; {0}".format(self.markers)).marker self.parsed_marker = markers + @property + def requirement_info(self): + # type: () -> Tuple(Optional[Text], Tuple[Optional[Text]], Optional[Text]) + """ + Generates a 3-tuple of the requisite *name*, *extras* and *url* to generate a + :class:`~packaging.requirements.Requirement` out of. + + :return: A Tuple containing an optional name, a Tuple of extras names, and an optional URL. + :rtype: Tuple[Optional[Text], Tuple[Optional[Text]], Optional[Text]] + """ + + # Direct URLs can be converted to packaging requirements directly, but + # only if they are `file://` (with only two slashes) + name = None + extras = () + url = None + # if self.is_direct_url: + if self._name: + name = canonicalize_name(self._name) + if self.is_file or self.is_url or self.is_path or self.is_file_url or self.is_vcs: + url = "" + if self.is_vcs: + url = self.url if self.url else self.uri + if self.is_direct_url: + url = self.link.url_without_fragment + else: + if self.link: + url = self.link.url_without_fragment + elif self.url: + url = self.url + if self.ref: + url = "{0}@{1}".format(url, self.ref) + else: + url = self.uri + if self.link and name is None: + self._name = self.link.egg_fragment + if self._name: + name = canonicalize_name(self._name) + # return "{0}{1}@ {2}".format( + # normalize_name(self.name), extras_to_string(self.extras), url + # ) + return (name, extras, url) + + @property + def line_is_installable(self): + # type: () -> bool + """ + This is a safeguard against decoy requirements when a user installs a package + whose name coincides with the name of a folder in the cwd, e.g. install *alembic* + when there is a folder called *alembic* in the working directory. + + In this case we first need to check that the given requirement is a valid + URL, VCS requirement, or installable filesystem path before deciding to treat it as + a file requirement over a named requirement. + """ + line = self.line + if is_file_url(line): + link = create_link(line) + line = link.url_without_fragment + line, _ = split_ref_from_uri(line) + if (is_vcs(line) or (is_valid_url(line) and ( + not is_file_url(line) or is_installable_file(line))) + or is_installable_file(line)): + return True + return False + def parse(self): # type: () -> None self.parse_hashes() @@ -660,8 +1032,13 @@ def parse(self): if self.line.startswith("git+file:/") and not self.line.startswith("git+file:///"): self.line = self.line.replace("git+file:/", "git+file:///") self.parse_markers() - if self.is_file: - self.populate_setup_paths() + if self.is_file_url: + if self.line_is_installable: + self.populate_setup_paths() + else: + raise RequirementError( + "Supplied requirement is not installable: {0!r}".format(self.line) + ) self.parse_link() self.parse_requirement() self.parse_ireq() @@ -669,10 +1046,10 @@ def parse(self): @attr.s(slots=True, hash=True) class NamedRequirement(object): - name = attr.ib() # type: str - version = attr.ib(validator=attr.validators.optional(validate_specifiers)) # type: Optional[str] + name = attr.ib() # type: Text + version = attr.ib() # type: Optional[Text] req = attr.ib() # type: PackagingRequirement - extras = attr.ib(default=attr.Factory(list)) # type: Tuple[str] + extras = attr.ib(default=attr.Factory(list)) # type: Tuple[Text] editable = attr.ib(default=False) # type: bool _parsed_line = attr.ib(default=None) # type: Optional[Line] @@ -693,9 +1070,9 @@ def parsed_line(self): @classmethod def from_line(cls, line, parsed_line=None): - # type: (str, Optional[Line]) -> NamedRequirement + # type: (Text, Optional[Line]) -> NamedRequirement req = init_requirement(line) - specifiers = None # type: Optional[str] + specifiers = None # type: Optional[Text] if req.specifier: specifiers = specs_to_string(req.specifier) req.line = line @@ -713,7 +1090,7 @@ def from_line(cls, line, parsed_line=None): "parsed_line": parsed_line, "extras": None } - extras = None # type: Optional[Tuple[str]] + extras = None # type: Optional[Tuple[Text]] if req.extras: extras = list(req.extras) creation_kwargs["extras"] = extras @@ -721,13 +1098,13 @@ def from_line(cls, line, parsed_line=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (str, Dict[str, Union[str, Optional[str], Optional[List[str]]]]) -> NamedRequirement - creation_args = {} # type: Dict[str, Union[Optional[str], Optional[List[str]]]] + # type: (Text, Dict[Text, Union[Text, Optional[Text], Optional[List[Text]]]]) -> NamedRequirement + creation_args = {} # type: Dict[Text, Union[Optional[Text], Optional[List[Text]]]] if hasattr(pipfile, "keys"): attr_fields = [field.name for field in attr.fields(cls)] creation_args = {k: v for k, v in pipfile.items() if k in attr_fields} creation_args["name"] = name - version = get_version(pipfile) # type: Optional[str] + version = get_version(pipfile) # type: Optional[Text] extras = creation_args.get("extras", None) creation_args["version"] = version req = init_requirement("{0}{1}".format(name, version)) @@ -738,15 +1115,15 @@ def from_pipfile(cls, name, pipfile): @property def line_part(self): - # type: () -> str + # type: () -> Text # FIXME: This should actually be canonicalized but for now we have to # simply lowercase it and replace underscores, since full canonicalization # also replaces dots and that doesn't actually work when querying the index - return "{0}".format(normalize_name(self.name)) + return normalize_name(self.name) @property def pipfile_part(self): - # type: () -> Dict[str, Any] + # type: () -> Dict[Text, Any] pipfile_dict = attr.asdict(self, filter=filter_none).copy() # type: ignore if "version" not in pipfile_dict: pipfile_dict["version"] = "*" @@ -761,42 +1138,42 @@ def pipfile_part(self): ) -@attr.s(slots=True, cmp=True) +@attr.s(slots=True, cmp=True, hash=True) class FileRequirement(object): """File requirements for tar.gz installable files or wheels or setup.py containing directories.""" #: Path to the relevant `setup.py` location - setup_path = attr.ib(default=None, cmp=True) # type: Optional[str] + setup_path = attr.ib(default=None, cmp=True) # type: Optional[Text] #: path to hit - without any of the VCS prefixes (like git+ / http+ / etc) - path = attr.ib(default=None, cmp=True) # type: Optional[str] + path = attr.ib(default=None, cmp=True) # type: Optional[Text] #: Whether the package is editable editable = attr.ib(default=False, cmp=True) # type: bool #: Extras if applicable - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[str] - _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[str] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[Text] + _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[Text] #: URI of the package - uri = attr.ib(cmp=True) # type: Optional[str] + uri = attr.ib(cmp=True) # type: Optional[Text] #: Link object representing the package to clone link = attr.ib(cmp=True) # type: Optional[Link] #: PyProject Requirements pyproject_requires = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple #: PyProject Build System - pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[str] + pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[Text] #: PyProject Path - pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[str] + pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[Text] #: Setup metadata e.g. dependencies _setup_info = attr.ib(default=None, cmp=True) # type: Optional[SetupInfo] _has_hashed_name = attr.ib(default=False, cmp=True) # type: bool - _parsed_line = attr.ib(default=None, hash=True) # type: Optional[Line] + _parsed_line = attr.ib(default=None, cmp=False, hash=True) # type: Optional[Line] #: Package name - name = attr.ib(cmp=True) # type: Optional[str] + name = attr.ib(cmp=True) # type: Optional[Text] #: A :class:`~pkg_resources.Requirement` isntance req = attr.ib(cmp=True) # type: Optional[PackagingRequirement] @classmethod def get_link_from_line(cls, line): - # type: (str) -> LinkInfo + # type: (Text) -> LinkInfo """Parse link information from given requirement line. Return a 6-tuple: @@ -830,16 +1207,16 @@ def get_link_from_line(cls, line): # Git allows `git@github.com...` lines that are not really URIs. # Add "ssh://" so we can parse correctly, and restore afterwards. - fixed_line = add_ssh_scheme_to_git_uri(line) # type: str + fixed_line = add_ssh_scheme_to_git_uri(line) # type: Text added_ssh_scheme = fixed_line != line # type: bool # We can assume a lot of things if this is a local filesystem path. if "://" not in fixed_line: p = Path(fixed_line).absolute() # type: Path - path = p.as_posix() # type: Optional[str] - uri = p.as_uri() # type: str + path = p.as_posix() # type: Optional[Text] + uri = p.as_uri() # type: Text link = create_link(uri) # type: Link - relpath = None # type: Optional[str] + relpath = None # type: Optional[Text] try: relpath = get_converted_relative_path(path) except ValueError: @@ -852,13 +1229,13 @@ def get_link_from_line(cls, line): original_url = parsed_url._replace() # type: SplitResult # Split the VCS part out if needed. - original_scheme = parsed_url.scheme # type: str - vcs_type = None # type: Optional[str] + original_scheme = parsed_url.scheme # type: Text + vcs_type = None # type: Optional[Text] if "+" in original_scheme: - scheme = None # type: Optional[str] + scheme = None # type: Optional[Text] vcs_type, _, scheme = original_scheme.partition("+") parsed_url = parsed_url._replace(scheme=scheme) - prefer = "uri" # type: str + prefer = "uri" # type: Text else: vcs_type = None prefer = "file" @@ -898,17 +1275,17 @@ def get_link_from_line(cls, line): @property def setup_py_dir(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self.setup_path: return os.path.dirname(os.path.abspath(self.setup_path)) return None @property def dependencies(self): - # type: () -> Tuple[Dict[str, PackagingRequirement], List[Union[str, PackagingRequirement]], List[str]] - build_deps = [] # type: List[Union[str, PackagingRequirement]] - setup_deps = [] # type: List[str] - deps = {} # type: Dict[str, PackagingRequirement] + # type: () -> Tuple[Dict[Text, PackagingRequirement], List[Union[Text, PackagingRequirement]], List[Text]] + build_deps = [] # type: List[Union[Text, PackagingRequirement]] + setup_deps = [] # type: List[Text] + deps = {} # type: Dict[Text, PackagingRequirement] if self.setup_info: setup_info = self.setup_info.as_dict() deps.update(setup_info.get("requires", {})) @@ -921,25 +1298,45 @@ def dependencies(self): return deps, setup_deps, build_deps def __attrs_post_init__(self): + # type: () -> None + if self.name is None and self.parsed_line: + if self.parsed_line.setup_info: + self._setup_info = self.parsed_line.setup_info + if self.parsed_line.setup_info.name: + self.name = self.parsed_line.setup_info.name + if self.req is None and self._parsed_line.requirement is not None: + self.req = self._parsed_line.requirement if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: if self.req is not None: self._parsed_line._ireq.req = self.req @property def setup_info(self): + # type: () -> SetupInfo from .setup_info import SetupInfo if self._setup_info is None and self.parsed_line: if self.parsed_line.setup_info: + if not self._parsed_line.setup_info.name: + self._parsed_line._setup_info.get_info() self._setup_info = self.parsed_line.setup_info - elif self.parsed_line.ireq: + elif self.parsed_line.ireq and not self.parsed_line.is_wheel: self._setup_info = SetupInfo.from_ireq(self.parsed_line.ireq) else: - self._setup_info = Line(self.line_part).setup_info + if self.link and not self.link.is_wheel: + self._setup_info = Line(self.line_part).setup_info + self._setup_info.get_info() return self._setup_info + @setup_info.setter + def setup_info(self, setup_info): + # type: (SetupInfo) -> None + self._setup_info = setup_info + if self._parsed_line: + self._parsed_line._setup_info = setup_info + @uri.default def get_uri(self): - # type: () -> str + # type: () -> Text if self.path and not self.uri: self._uri_scheme = "path" return pip_shims.shims.path_to_url(os.path.abspath(self.path)) @@ -951,7 +1348,7 @@ def get_uri(self): @name.default def get_name(self): - # type: () -> str + # type: () -> Text loc = self.path or self.uri if loc and not self._uri_scheme: self._uri_scheme = "path" if self.path else "file" @@ -1023,7 +1420,7 @@ def get_name(self): @link.default def get_link(self): - # type: () -> Link + # type: () -> pip_shims.shims.Link target = "{0}".format(self.uri) if hasattr(self, "name") and not self._has_hashed_name: target = "{0}#egg={1}".format(target, self.name) @@ -1032,7 +1429,7 @@ def get_link(self): @req.default def get_requirement(self): - # type: () -> PackagingRequirement + # type: () -> RequirementType if self.name is None: if self._parsed_line is not None and self._parsed_line.name is not None: self.name = self._parsed_line.name @@ -1040,6 +1437,15 @@ def get_requirement(self): raise ValueError( "Failed to generate a requirement: missing name for {0!r}".format(self) ) + if self._parsed_line: + try: + # initialize specifiers to make sure we capture them + self._parsed_line.specifiers + except Exception: + pass + req = copy.deepcopy(self._parsed_line.requirement) + return req + req = init_requirement(normalize_name(self.name)) req.editable = False if self.link is not None: @@ -1109,7 +1515,7 @@ def is_direct_url(self): @property def formatted_path(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self.path: path = self.path if not isinstance(path, Path): @@ -1120,16 +1526,16 @@ def formatted_path(self): @classmethod def create( cls, - path=None, # type: Optional[str] - uri=None, # type: str + path=None, # type: Optional[Text] + uri=None, # type: Text editable=False, # type: bool - extras=None, # type: Optional[Tuple[str]] + extras=None, # type: Optional[Tuple[Text]] link=None, # type: Link vcs_type=None, # type: Optional[Any] - name=None, # type: Optional[str] + name=None, # type: Optional[Text] req=None, # type: Optional[Any] - line=None, # type: Optional[str] - uri_scheme=None, # type: str + line=None, # type: Optional[Text] + uri_scheme=None, # type: Text setup_path=None, # type: Optional[Any] relpath=None, # type: Optional[Any] parsed_line=None, # type: Optional[Line] @@ -1190,27 +1596,27 @@ def create( creation_kwargs["vcs"] = vcs_type if name: creation_kwargs["name"] = name - _line = None - ireq = None - setup_info = None + _line = None # type: Optional[Text] + ireq = None # type: Optional[InstallRequirement] + setup_info = None # type: Optional[SetupInfo] if parsed_line: if parsed_line.name: name = parsed_line.name if parsed_line.setup_info: name = parsed_line.setup_info.as_dict().get("name", name) if not name or not parsed_line: - if link is not None and link.url is not None: + if link is not None and link.url_without_fragment is not None: _line = unquote(link.url_without_fragment) if name: _line = "{0}#egg={1}".format(_line, name) - # if extras: - # _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) + if extras and extras_to_string(extras) not in _line: + _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) elif uri is not None: - _line = uri + _line = unquote(uri) else: - _line = line + _line = unquote(line) if editable: - if extras and ( + if extras and extras_to_string(extras) not in _line and ( (link and link.scheme == "file") or (uri and uri.startswith("file")) or (not uri and not link) ): @@ -1219,7 +1625,7 @@ def create( ireq = pip_shims.shims.install_req_from_editable(_line) else: _line = path if (uri_scheme and uri_scheme == "path") else _line - if extras: + if extras and extras_to_string(extras) not in _line: _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) if ireq is None: ireq = pip_shims.shims.install_req_from_line(_line) @@ -1228,16 +1634,16 @@ def create( parsed_line = Line(_line) if ireq is None: ireq = parsed_line.ireq - if extras and not ireq.extras: + if extras and ireq is not None and not ireq.extras: ireq.extras = set(extras) if setup_info is None: setup_info = SetupInfo.from_ireq(ireq) setupinfo_dict = setup_info.as_dict() setup_name = setupinfo_dict.get("name", None) - if setup_name: + if setup_name is not None: name = setup_name build_requires = setupinfo_dict.get("build_requires", ()) - build_backend = setupinfo_dict.get("build_backend", ()) + build_backend = setupinfo_dict.get("build_backend", "") if not creation_kwargs.get("pyproject_requires") and build_requires: creation_kwargs["pyproject_requires"] = tuple(build_requires) if not creation_kwargs.get("pyproject_backend") and build_backend: @@ -1271,7 +1677,7 @@ def create( @classmethod def from_line(cls, line, extras=None, parsed_line=None): - # type: (str, Optional[Tuple[str]], Optional[Line]) -> FileRequirement + # type: (Text, Optional[Tuple[Text]], Optional[Line]) -> FileRequirement line = line.strip('"').strip("'") link = None path = None @@ -1282,6 +1688,8 @@ def from_line(cls, line, extras=None, parsed_line=None): req = None if not extras: extras = () + else: + extras = tuple(extras) if not any([is_installable_file(line), is_valid_url(line), is_file_url(line)]): try: req = init_requirement(line) @@ -1320,7 +1728,7 @@ def from_line(cls, line, extras=None, parsed_line=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (str, Dict[str, Any]) -> FileRequirement + # type: (Text, Dict[Text, Any]) -> FileRequirement # Parse the values out. After this dance we should have two variables: # path - Local filesystem path. # uri - Absolute URI that is parsable with urlsplit. @@ -1360,19 +1768,31 @@ def from_pipfile(cls, name, pipfile): "editable": pipfile.get("editable", False), "link": link, "uri_scheme": uri_scheme, - "extras": pipfile.get("extras", None) + "extras": pipfile.get("extras", None), } extras = pipfile.get("extras", ()) + if extras: + extras = tuple(extras) line = "" - if name: + if pipfile.get("editable", False) and uri_scheme == "path": + line = "{0}".format(path) if extras: - line_name = "{0}[{1}]".format(name, ",".join(sorted(set(extras)))) - else: - line_name = "{0}".format(name) - line = "{0}@ {1}".format(line_name, link.url_without_fragment) + line = "{0}{1}".format(line, extras_to_string(extras)) else: - line = link.url + if name: + if extras: + line_name = "{0}{1}".format(name, extras_to_string(extras)) + else: + line_name = "{0}".format(name) + line = "{0}#egg={1}".format(unquote(link.url_without_fragment), line_name) + else: + line = unquote(link.url) + if extras: + line = "{0}{1}".format(line, extras_to_string(extras)) + if "subdirectory" in pipfile: + arg_dict["subdirectory"] = pipfile["subdirectory"] + line = "{0}&subdirectory={1}".format(pipfile["subdirectory"]) if pipfile.get("editable", False): line = "-e {0}".format(line) arg_dict["line"] = line @@ -1380,9 +1800,9 @@ def from_pipfile(cls, name, pipfile): @property def line_part(self): - # type: () -> str - link_url = None # type: Optional[str] - seed = None # type: Optional[str] + # type: () -> Text + link_url = None # type: Optional[Text] + seed = None # type: Optional[Text] if self.link is not None: link_url = unquote(self.link.url_without_fragment) if self._uri_scheme and self._uri_scheme == "path": @@ -1400,10 +1820,9 @@ def line_part(self): raise ValueError("Could not calculate url for {0!r}".format(self)) return "{0}{1}".format(editable, seed) - @property def pipfile_part(self): - # type: () -> Dict[str, Dict[str, Any]] + # type: () -> Dict[Text, Dict[Text, Any]] excludes = [ "_base_line", "_has_hashed_name", "setup_path", "pyproject_path", "_uri_scheme", "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" @@ -1415,7 +1834,7 @@ def pipfile_part(self): pipfile_dict.pop("_uri_scheme") # For local paths and remote installable artifacts (zipfiles, etc) collision_keys = {"file", "uri", "path"} - collision_order = ["file", "uri", "path"] # type: List[str] + collision_order = ["file", "uri", "path"] # type: List[Text] key_match = next(iter(k for k in collision_order if k in pipfile_dict.keys())) if self._uri_scheme: dict_key = self._uri_scheme @@ -1457,35 +1876,36 @@ class VCSRequirement(FileRequirement): #: Whether the repository is editable editable = attr.ib(default=None) # type: Optional[bool] #: URI for the repository - uri = attr.ib(default=None) # type: Optional[str] + uri = attr.ib(default=None) # type: Optional[Text] #: path to the repository, if it's local - path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) # type: Optional[str] + path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) # type: Optional[Text] #: vcs type, i.e. git/hg/svn - vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) # type: Optional[str] + vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) # type: Optional[Text] #: vcs reference name (branch / commit / tag) - ref = attr.ib(default=None) # type: Optional[str] + ref = attr.ib(default=None) # type: Optional[Text] #: Subdirectory to use for installation if applicable - subdirectory = attr.ib(default=None) # type: Optional[str] - _repo = attr.ib(default=None) # type: Optional['VCSRepository'] - _base_line = attr.ib(default=None) # type: Optional[str] - name = attr.ib() - link = attr.ib() - req = attr.ib() + subdirectory = attr.ib(default=None) # type: Optional[Text] + _repo = attr.ib(default=None) # type: Optional[VCSRepository] + _base_line = attr.ib(default=None) # type: Optional[Text] + name = attr.ib() # type: Text + link = attr.ib() # type: Optional[pip_shims.shims.Link] + req = attr.ib() # type: Optional[RequirementType] def __attrs_post_init__(self): # type: () -> None if not self.uri: if self.path: self.uri = pip_shims.shims.path_to_url(self.path) - split = urllib_parse.urlsplit(self.uri) - scheme, rest = split[0], split[1:] - vcs_type = "" - if "+" in scheme: - vcs_type, scheme = scheme.split("+", 1) - vcs_type = "{0}+".format(vcs_type) - new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) - new_uri = "{0}{1}".format(vcs_type, new_uri) - self.uri = new_uri + if self.uri is not None: + split = urllib_parse.urlsplit(self.uri) + scheme, rest = split[0], split[1:] + vcs_type = "" + if "+" in scheme: + vcs_type, scheme = scheme.split("+", 1) + vcs_type = "{0}+".format(vcs_type) + new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) + new_uri = "{0}{1}".format(vcs_type, new_uri) + self.uri = new_uri if self.req and ( self.parsed_line.ireq and not self.parsed_line.ireq.req ): @@ -1507,7 +1927,7 @@ def get_link(self): @name.default def get_name(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] return ( self.link.egg_fragment or self.req.name if getattr(self, "req", None) @@ -1516,15 +1936,37 @@ def get_name(self): @property def vcs_uri(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] uri = self.uri if not any(uri.startswith("{0}+".format(vcs)) for vcs in VCS_LIST): uri = "{0}+{1}".format(self.vcs, uri) return uri + @property + def setup_info(self): + if self._repo: + from .setup_info import SetupInfo + self._setup_info = SetupInfo.from_ireq(Line(self._repo.checkout_directory).ireq) + self._setup_info.get_info() + return self._setup_info + if self._parsed_line and self._parsed_line.setup_info: + if not self._parsed_line.setup_info.name: + self._parsed_line._setup_info.get_info() + return self._parsed_line.setup_info + ireq = self.parsed_line.ireq + from .setup_info import SetupInfo + self._setup_info = SetupInfo.from_ireq(ireq) + return self._setup_info + + @setup_info.setter + def setup_info(self, setup_info): + self._setup_info = setup_info + if self._parsed_line: + self._parsed_line.setup_info = setup_info + @req.default def get_requirement(self): - # type: () -> PkgResourcesRequirement + # type: () -> PackagingRequirement name = self.name or self.link.egg_fragment url = None if self.uri: @@ -1578,10 +2020,12 @@ def repo(self): # type: () -> VCSRepository if self._repo is None: self._repo = self.get_vcs_repo() + if self._parsed_line: + self._parsed_line.vcsrepo = self._repo return self._repo def get_checkout_dir(self, src_dir=None): - # type: (Optional[str]) -> str + # type: (Optional[Text]) -> Text src_dir = os.environ.get("PIP_SRC", None) if not src_dir else src_dir checkout_dir = None if self.is_local: @@ -1598,7 +2042,7 @@ def get_checkout_dir(self, src_dir=None): return os.path.join(create_tracked_tempdir(prefix="requirementslib"), self.name) def get_vcs_repo(self, src_dir=None): - # type: (Optional[str]) -> VCSRepository + # type: (Optional[Text]) -> VCSRepository from .vcs import VCSRepository checkout_dir = self.get_checkout_dir(src_dir=src_dir) @@ -1628,13 +2072,13 @@ def get_vcs_repo(self, src_dir=None): return vcsrepo def get_commit_hash(self): - # type: () -> str + # type: () -> Text hash_ = None hash_ = self.repo.get_commit_hash() return hash_ def update_repo(self, src_dir=None, ref=None): - # type: (Optional[str], Optional[str]) -> str + # type: (Optional[Text], Optional[Text]) -> Text if ref: self.ref = ref else: @@ -1649,20 +2093,40 @@ def update_repo(self, src_dir=None, ref=None): @contextmanager def locked_vcs_repo(self, src_dir=None): - # type: (Optional[str]) -> Generator[VCSRepository, None, None] + # type: (Optional[Text]) -> Generator[VCSRepository, None, None] if not src_dir: src_dir = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") vcsrepo = self.get_vcs_repo(src_dir=src_dir) - self.req.revision = vcsrepo.get_commit_hash() + if not self.req: + if self.parsed_line is not None: + self.req = self.parsed_line.requirement + else: + self.req = self.get_requirement() + revision = self.req.revision = vcsrepo.get_commit_hash() # Remove potential ref in the end of uri after ref is parsed if "@" in self.link.show_url and "@" in self.uri: - uri, ref = self.uri.rsplit("@", 1) - checkout = self.req.revision - if checkout and ref in checkout: + uri, ref = split_ref_from_uri(self.uri) + checkout = revision + if checkout and ref and ref in checkout: self.uri = uri orig_repo = self._repo self._repo = vcsrepo + if self._parsed_line: + self._parsed_line.vcsrepo = vcsrepo + if self._setup_info: + self._setup_info._requirements = () + self._setup_info._extras_requirements = () + self._setup_info.build_requires = () + self._setup_info.setup_requires = () + self._setup_info.version = None + self._setup_info.metadata = None + if self.parsed_line: + self._parsed_line.vcsrepo = vcsrepo + # self._parsed_line._specifier = "=={0}".format(self.setup_info.version) + # self._parsed_line.specifiers = self._parsed_line._specifier + if self.req: + self.req.specifier = SpecifierSet("=={0}".format(self.setup_info.version)) try: yield vcsrepo finally: @@ -1670,7 +2134,7 @@ def locked_vcs_repo(self, src_dir=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (str, Dict[str, Union[List[str], str, bool]]) -> VCSRequirement + # type: (Text, Dict[Text, Union[List[Text], Text, bool]]) -> VCSRequirement creation_args = {} pipfile_keys = [ k @@ -1715,7 +2179,16 @@ def from_pipfile(cls, name, pipfile): creation_args["name"] = name cls_inst = cls(**creation_args) if cls_inst._parsed_line is None: - cls_inst._parsed_line = Line(cls_inst.line_part) + vcs_uri = build_vcs_uri( + vcs=cls_inst.vcs, uri=add_ssh_scheme_to_git_uri(cls_inst.uri), + name=cls_inst.name, ref=cls_inst.ref, subdirectory=cls_inst.subdirectory, + extras=cls_inst.extras + ) + if cls_inst.editable: + vcs_uri = "-e {0}".format(vcs_uri) + cls_inst._parsed_line = Line(vcs_uri) + if not cls_inst.name and cls_inst._parsed_line.name: + cls_inst.name = cls_inst._parsed_line.name if cls_inst.req and ( cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req ): @@ -1724,7 +2197,7 @@ def from_pipfile(cls, name, pipfile): @classmethod def from_line(cls, line, editable=None, extras=None, parsed_line=None): - # type: (str, Optional[bool], Optional[Tuple[str]], Optional[Line]) -> VCSRequirement + # type: (Text, Optional[bool], Optional[Tuple[Text]], Optional[Line]) -> VCSRequirement relpath = None if parsed_line is None: parsed_line = Line(line) @@ -1757,14 +2230,14 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): extras = tuple(extras) subdirectory = link.subdirectory_fragment ref = None - if "@" in link.path and "@" in uri: - uri, _, ref = uri.rpartition("@") + if uri: + uri, ref = split_ref_from_uri(uri) if path is not None and "@" in path: - path, _ref = path.rsplit("@", 1) + path, _ref = split_ref_from_uri(path) if ref is None: ref = _ref if relpath and "@" in relpath: - relpath, ref = relpath.rsplit("@", 1) + relpath, ref = split_ref_from_uri(relpath) creation_args = { "name": name if name else parsed_line.name, @@ -1802,7 +2275,7 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): @property def line_part(self): - # type: () -> str + # type: () -> Text """requirements.txt compatible line part sans-extras""" if self.is_local: base_link = self.link @@ -1827,13 +2300,13 @@ def line_part(self): base = "{0}{1}".format(base, extras_to_string(sorted(self.extras))) if "git+file:/" in base and "git+file:///" not in base: base = base.replace("git+file:/", "git+file:///") - if self.editable: + if self.editable and not base.startswith("-e "): base = "-e {0}".format(base) return base @staticmethod def _choose_vcs_source(pipfile): - # type: (Dict[str, Union[List[str], str, bool]]) -> Dict[str, Union[List[str], str, bool]] + # type: (Dict[Text, Union[List[Text], Text, bool]]) -> Dict[Text, Union[List[Text], Text, bool]] src_keys = [k for k in pipfile.keys() if k in ["path", "uri", "file"]] if src_keys: chosen_key = first(src_keys) @@ -1846,10 +2319,11 @@ def _choose_vcs_source(pipfile): @property def pipfile_part(self): - # type: () -> Dict[str, Dict[str, Union[List[str], str, bool]]] + # type: () -> Dict[Text, Dict[Text, Union[List[Text], Text, bool]]] excludes = [ "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", - "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" + "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line", + "_uri_scheme" ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() @@ -1859,27 +2333,27 @@ def pipfile_part(self): return {name: pipfile_dict} -@attr.s(cmp=True) +@attr.s(cmp=True, hash=True) class Requirement(object): - name = attr.ib(cmp=True) # type: str - vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[str] + name = attr.ib(cmp=True) # type: Text + vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[Text] req = attr.ib(default=None, cmp=True) # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] - markers = attr.ib(default=None, cmp=True) # type: Optional[str] - _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[str] - index = attr.ib(default=None) # type: Optional[str] + markers = attr.ib(default=None, cmp=True) # type: Optional[Text] + _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[Text] + index = attr.ib(default=None, cmp=True) # type: Optional[Text] editable = attr.ib(default=None, cmp=True) # type: Optional[bool] - hashes = attr.ib(default=attr.Factory(tuple), converter=tuple, cmp=True) # type: Optional[Tuple[str]] - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[str]] + hashes = attr.ib(factory=frozenset, converter=frozenset, cmp=True) # type: Optional[Tuple[Text]] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[Text]] abstract_dep = attr.ib(default=None, cmp=False) # type: Optional[AbstractDependency] - _line_instance = attr.ib(default=None, cmp=True) # type: Optional[Line] - _ireq = attr.ib(default=None) # type: Optional[pip_shims.InstallRequirement] + _line_instance = attr.ib(default=None, cmp=False) # type: Optional[Line] + _ireq = attr.ib(default=None, cmp=False) # type: Optional[pip_shims.InstallRequirement] def __hash__(self): return hash(self.as_line()) @name.default def get_name(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] return self.req.name @property @@ -1887,8 +2361,16 @@ def requirement(self): # type: () -> Optional[PackagingRequirement] return self.req.req + def add_hashes(self, hashes): + # type: (Union[List, Set, Tuple]) -> Requirement + if isinstance(hashes, six.string_types): + new_hashes = set(self.hashes).add(hashes) + else: + new_hashes = set(self.hashes) | set(hashes) + return attr.evolve(self, hashes=frozenset(new_hashes)) + def get_hashes_as_pip(self, as_list=False): - # type: () -> Union[str, List[str]] + # type: (bool) -> Union[Text, List[Text]] if self.hashes: if as_list: return [HASH_STRING.format(h) for h in self.hashes] @@ -1897,12 +2379,12 @@ def get_hashes_as_pip(self, as_list=False): @property def hashes_as_pip(self): - # type: () -> Union[str, List[str]] + # type: () -> Union[Text, List[Text]] self.get_hashes_as_pip() @property def markers_as_pip(self): - # type: () -> str + # type: () -> Text if self.markers: return " ; {0}".format(self.markers).replace('"', "'") @@ -1910,7 +2392,7 @@ def markers_as_pip(self): @property def extras_as_pip(self): - # type: () -> str + # type: () -> Text if self.extras: return "[{0}]".format( ",".join(sorted([extra.lower() for extra in self.extras])) @@ -1920,7 +2402,7 @@ def extras_as_pip(self): @property def commit_hash(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if not self.is_vcs: return None commit_hash = None @@ -1930,34 +2412,58 @@ def commit_hash(self): @_specifiers.default def get_specifiers(self): - # type: () -> Optional[str] + # type: () -> Text if self.req and self.req.req and self.req.req.specifier: return specs_to_string(self.req.req.specifier) return "" + def update_name_from_path(self, path): + from .setup_info import get_metadata + metadata = get_metadata(path) + name = self.name + if metadata is not None: + name = metadata.get("name") + if name is not None: + if self.req.name is None: + self.req.name = name + if self.req.req and self.req.req.name is None: + self.req.req.name = name + if self._line_instance._name is None: + self._line_instance.name = name + if self.req._parsed_line._name is None: + self.req._parsed_line.name = name + if self.req._setup_info and self.req._setup_info.name is None: + self.req._setup_info.name = name + @property def line_instance(self): # type: () -> Optional[Line] - include_extras = True - include_specifiers = True - if self.is_vcs: - include_extras = False - if self.is_file_or_url or self.is_vcs or not self._specifiers: - include_specifiers = False - if self._line_instance is None: - parts = [ - self.req.line_part, - self.extras_as_pip if include_extras else "", - self._specifiers if include_specifiers else "", - self.markers_as_pip, - ] - self._line_instance = Line("".join(parts)) + if self.req.parsed_line is not None: + self._line_instance = self.req.parsed_line + else: + include_extras = True + include_specifiers = True + if self.is_vcs: + include_extras = False + if self.is_file_or_url or self.is_vcs or not self._specifiers: + include_specifiers = False + + parts = [ + self.req.line_part, + self.extras_as_pip if include_extras else "", + self._specifiers if include_specifiers else "", + self.markers_as_pip, + ] + line = "".join(parts) + if line is None: + return None + self._line_instance = Line(line) return self._line_instance @property def specifiers(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self._specifiers: return self._specifiers else: @@ -1965,10 +2471,11 @@ def specifiers(self): if specs: self._specifiers = specs return specs - if not self._specifiers and self.req and self.req.req and self.req.req.specifier: - self._specifiers = specs_to_string(self.req.req.specifier) - elif self.is_named and not self._specifiers: + if self.is_named and not self._specifiers: self._specifiers = self.req.version + elif not self.editable and not self.is_named: + if self.line_instance and self.line_instance.setup_info and self.line_instance.setup_info.version: + self._specifiers = "=={0}".format(self.req.setup_info.version) elif self.req.parsed_line.specifiers and not self._specifiers: self._specifiers = specs_to_string(self.req.parsed_line.specifiers) elif self.line_instance.specifiers and not self._specifiers: @@ -1997,7 +2504,7 @@ def is_vcs(self): @property def build_backend(self): - # type: () -> Optional[str] + # type: () -> Optional[Text] if self.is_vcs or (self.is_file_or_url and self.req.is_local): setup_info = self.run_requires() build_backend = setup_info.get("build_backend") @@ -2021,8 +2528,16 @@ def is_named(self): # type: () -> bool return isinstance(self.req, NamedRequirement) + @property + def is_wheel(self): + # type: () -> bool + if not self.is_named and self.req.link is not None and self.req.link.is_wheel: + return True + return False + @property def normalized_name(self): + # type: () -> Text return canonicalize_name(self.name) def copy(self): @@ -2030,88 +2545,98 @@ def copy(self): @classmethod def from_line(cls, line): - # type: (str) -> Requirement + # type: (Text) -> Requirement if isinstance(line, pip_shims.shims.InstallRequirement): line = format_requirement(line) - hashes = None - if "--hash=" in line: - hashes = line.split(" --hash=") - line, hashes = hashes[0], hashes[1:] - line_instance = Line(line) - editable = line.startswith("-e ") - line = line.split(" ", 1)[1] if editable else line - line, markers = split_markers_from_line(line) - line, extras = pip_shims.shims._strip_extras(line) - if extras: - extras = tuple(parse_extras(extras)) - line = line.strip('"').strip("'").strip() - line_with_prefix = "-e {0}".format(line) if editable else line - vcs = None - # Installable local files and installable non-vcs urls are handled - # as files, generally speaking - line_is_vcs = is_vcs(line) - is_direct_url = False - # check for pep-508 compatible requirements - name, _, possible_url = line.partition("@") - name = name.strip() - if possible_url is not None: - possible_url = possible_url.strip() - is_direct_url = is_valid_url(possible_url) - if not line_is_vcs: - line_is_vcs = is_vcs(possible_url) + parsed_line = Line(line) r = None # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] - if is_installable_file(line) or ( - (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and - not (line_is_vcs or is_vcs(possible_url)) - ): - r = FileRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) - elif line_is_vcs: - r = VCSRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) - if isinstance(r, VCSRequirement): - vcs = r.vcs + if ((parsed_line.is_file and parsed_line.is_installable) or parsed_line.is_url) and not parsed_line.is_vcs: + r = file_req_from_parsed_line(parsed_line) + elif parsed_line.is_vcs: + r = vcs_req_from_parsed_line(parsed_line) elif line == "." and not is_installable_file(line): raise RequirementError( "Error parsing requirement %s -- are you sure it is installable?" % line ) else: - specs = "!=<>~" - spec_matches = set(specs) & set(line) - version = None - name = "{0}".format(line) - if spec_matches: - spec_idx = min((line.index(match) for match in spec_matches)) - name = line[:spec_idx] - version = line[spec_idx:] - if not extras: - name, extras = pip_shims.shims._strip_extras(name) - if extras: - extras = tuple(parse_extras(extras)) - if version: - name = "{0}{1}".format(name, version) - r = NamedRequirement.from_line(line, parsed_line=line_instance) + r = named_req_from_parsed_line(parsed_line) + # hashes = None + # if "--hash=" in line: + # hashes = line.split(" --hash=") + # line, hashes = hashes[0], hashes[1:] + # editable = line.startswith("-e ") + # line = line.split(" ", 1)[1] if editable else line + # line, markers = split_markers_from_line(line) + # line, extras = pip_shims.shims._strip_extras(line) + # if extras: + # extras = tuple(parse_extras(extras)) + # line = line.strip('"').strip("'").strip() + # line_with_prefix = "-e {0}".format(line) if editable else line + # vcs = None + # # Installable local files and installable non-vcs urls are handled + # # as files, generally speaking + # line_is_vcs = is_vcs(line) + # is_direct_url = False + # # check for pep-508 compatible requirements + # name, _, possible_url = line.partition("@") + # name = name.strip() + # if possible_url is not None: + # possible_url = possible_url.strip() + # is_direct_url = is_valid_url(possible_url) + # if not line_is_vcs: + # line_is_vcs = is_vcs(possible_url) + # if is_installable_file(line) or ( + # (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and + # not (line_is_vcs or is_vcs(possible_url)) + # ): + # r = FileRequirement.from_line(line_with_prefix, extras=extras, parsed_line=parsed_line) + # elif line_is_vcs: + # r = VCSRequirement.from_line(line_with_prefix, extras=extras, parsed_line=parsed_line) + # if isinstance(r, VCSRequirement): + # vcs = r.vcs + # elif line == "." and not is_installable_file(line): + # raise RequirementError( + # "Error parsing requirement %s -- are you sure it is installable?" % line + # ) + # else: + # specs = "!=<>~" + # spec_matches = set(specs) & set(line) + # version = None + # name = "{0}".format(line) + # if spec_matches: + # spec_idx = min((line.index(match) for match in spec_matches)) + # name = line[:spec_idx] + # version = line[spec_idx:] + # if not extras: + # name, extras = pip_shims.shims._strip_extras(name) + # if extras: + # extras = tuple(parse_extras(extras)) + # if version: + # name = "{0}{1}".format(name, version) + # r = NamedRequirement.from_line(line, parsed_line=parsed_line) req_markers = None - if markers: - req_markers = PackagingRequirement("fakepkg; {0}".format(markers)) + if parsed_line.markers: + req_markers = PackagingRequirement("fakepkg; {0}".format(parsed_line.markers)) if r is not None and r.req is not None: r.req.marker = getattr(req_markers, "marker", None) if req_markers else None - r.req.local_file = getattr(r.req, "local_file", False) - name = getattr(r, "name", None) - if name is None and getattr(r.req, "name", None) is not None: - name = r.req.name - elif name is None and getattr(r.req, "key", None) is not None: - name = r.req.key - if name is not None and getattr(r.req, "name", None) is None: - r.req.name = name + # r.req.local_file = getattr(r.req, "local_file", False) + # name = getattr(r, "name", None) + # if name is None and getattr(r.req, "name", None) is not None: + # name = r.req.name + # elif name is None and getattr(r.req, "key", None) is not None: + # name = r.req.key + # if name is not None and getattr(r.req, "name", None) is None: + # r.req.name = name args = { - "name": name, - "vcs": vcs, + "name": r.name, + "vcs": parsed_line.vcs, "req": r, - "markers": markers, - "editable": editable, - "line_instance": line_instance + "markers": parsed_line.markers, + "editable": parsed_line.editable, + "line_instance": parsed_line } - if extras: - extras = tuple(sorted(dedup([extra.lower() for extra in extras]))) + if parsed_line.extras: + extras = tuple(sorted(dedup([extra.lower() for extra in parsed_line.extras]))) args["extras"] = extras if r is not None: r.extras = extras @@ -2119,8 +2644,8 @@ def from_line(cls, line): args["extras"] = tuple(sorted(dedup([extra.lower() for extra in r.extras]))) # type: ignore if r.req is not None: r.req.extras = args["extras"] - if hashes: - args["hashes"] = tuple(hashes) # type: ignore + if parsed_line.hashes: + args["hashes"] = tuple(parsed_line.hashes) # type: ignore cls_inst = cls(**args) return cls_inst @@ -2158,22 +2683,35 @@ def from_pipfile(cls, name, pipfile): if r.req is not None: r.req.marker = req_markers.marker extras = _pipfile.get("extras") - r.req.specifier = SpecifierSet(_pipfile["version"]) - r.req.extras = ( - tuple(sorted(dedup([extra.lower() for extra in extras]))) if extras else () - ) + if r.req: + if r.req.specifier: + r.req.specifier = SpecifierSet(_pipfile["version"]) + r.req.extras = ( + tuple(sorted(dedup([extra.lower() for extra in extras]))) if extras else () + ) args = { "name": r.name, "vcs": vcs, "req": r, "markers": markers, - "extras": tuple(_pipfile.get("extras", [])), + "extras": tuple(_pipfile.get("extras", ())), "editable": _pipfile.get("editable", False), "index": _pipfile.get("index"), } if any(key in _pipfile for key in ["hash", "hashes"]): args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")]) cls_inst = cls(**args) + # if not cls_inst.req._parsed_line: + # parsed_line = Line(cls_inst.as_line()) + # cls_inst.req._parsed_line = parsed_line + # if not cls_inst.line_instance: + # cls_inst.line_instance = parsed_line + # if not cls_inst.is_named and not cls_inst.req._setup_info and parsed_line.setup_info: + # cls_inst.req._setup_info = parsed_line.setup_info + # if not cls_inst.req.name and parsed_line.setup_info.name: + # cls_inst.name = cls_inst.req.name = parsed_line.setup_info.name + # if not cls_inst.req.name and parsed_line.name: + # cls_inst.name = cls_inst.req.name = parsed_line.name return cls_inst def as_line( @@ -2234,7 +2772,7 @@ def get_markers(self): markers = self.markers if markers: fake_pkg = PackagingRequirement("fakepkg; {0}".format(markers)) - markers = fake_pkg.markers + markers = fake_pkg.marker return markers def get_specifier(self): @@ -2444,3 +2982,69 @@ def merge_markers(self, markers): new_markers = Marker(" or ".join([str(m) for m in sorted(_markers)])) self.markers = str(new_markers) self.req.req.marker = new_markers + + +def file_req_from_parsed_line(parsed_line): + # type: (Line) -> FileRequirement + path = parsed_line.relpath if parsed_line.relpath else parsed_line.path + return FileRequirement( + setup_path=parsed_line.setup_py, + path=path, + editable=parsed_line.editable, + extras=parsed_line.extras, + uri_scheme=parsed_line.preferred_scheme, + link=parsed_line.link, + uri=parsed_line.uri, + pyproject_requires=tuple(parsed_line.pyproject_requires) if parsed_line.pyproject_requires else None, + pyproject_backend=parsed_line.pyproject_backend, + pyproject_path=Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, + parsed_line=parsed_line, + name=parsed_line.name, + req=parsed_line.requirement + ) + + +def vcs_req_from_parsed_line(parsed_line): + # type: (Line) -> VCSRequirement + line = "{0}".format(parsed_line.line) + if parsed_line.editable: + line = "-e {0}".format(line) + link = create_link(build_vcs_uri( + vcs=parsed_line.vcs, + uri=parsed_line.url, + name=parsed_line.name, + ref=parsed_line.ref, + subdirectory=parsed_line.subdirectory, + extras=parsed_line.extras + )) + return VCSRequirement( + setup_path=parsed_line.setup_py, + path=parsed_line.path, + editable=parsed_line.editable, + vcs=parsed_line.vcs, + ref=parsed_line.ref, + subdirectory=parsed_line.subdirectory, + extras=parsed_line.extras, + uri_scheme=parsed_line.preferred_scheme, + link=link, + uri=parsed_line.uri, + pyproject_requires=tuple(parsed_line.pyproject_requires) if parsed_line.pyproject_requires else None, + pyproject_backend=parsed_line.pyproject_backend, + pyproject_path=Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, + parsed_line=parsed_line, + name=parsed_line.name, + req=parsed_line.requirement, + base_line=line, + ) + + +def named_req_from_parsed_line(parsed_line): + # type: (Line) -> NamedRequirement + return NamedRequirement( + name=parsed_line.name, + version=parsed_line.specifier, + req=parsed_line.requirement, + extras=parsed_line.extras, + editable=parsed_line.editable, + parsed_line=parsed_line + ) diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 48d883b9..315b9a96 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -1,32 +1,45 @@ # -*- coding=utf-8 -*- from __future__ import absolute_import, print_function +import atexit import contextlib import os +import shutil import sys import attr -import packaging.version import packaging.specifiers import packaging.utils +import packaging.version +import pep517.envbuild +import pep517.wrappers import six - -try: - from setuptools.dist import distutils -except ImportError: - import distutils - from appdirs import user_cache_dir +from distlib.wheel import Wheel +from packaging.markers import Marker from six.moves import configparser -from six.moves.urllib.parse import unquote -from vistir.compat import Path, Iterable -from vistir.contextmanagers import cd, temp_environ +from six.moves.urllib.parse import unquote, urlparse, urlunparse + +from vistir.compat import Iterable, Path +from vistir.contextmanagers import cd, temp_path, replaced_streams from vistir.misc import run -from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p +from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p, rmtree -from .utils import init_requirement, get_pyproject, get_name_variants from ..environment import MYPY_RUNNING from ..exceptions import RequirementError +from .utils import ( + get_name_variants, + get_pyproject, + init_requirement, + split_vcs_method_from_uri, + strip_extras_markers_from_requirement +) + +try: + from setuptools.dist import distutils +except ImportError: + import distutils + try: from os import scandir @@ -35,9 +48,15 @@ if MYPY_RUNNING: - from typing import Any, Dict, List, Generator, Optional, Union - from pip_shims.shims import InstallRequirement - from pkg_resources import Requirement as PkgResourcesRequirement + from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text + from pip_shims.shims import InstallRequirement, PackageFinder + from pkg_resources import ( + PathMetadata, DistInfoDistribution, Requirement as PkgResourcesRequirement + ) + from packaging.requirements import Requirement as PackagingRequirement + TRequirement = TypeVar("TRequirement") + RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) + MarkerType = TypeVar('MarkerType', covariant=True, bound=Marker) CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv")) @@ -77,7 +96,7 @@ def _get_src_dir(root): virtual_env = os.environ.get("VIRTUAL_ENV") if virtual_env is not None: return os.path.join(virtual_env, "src") - if not root: + if root is not None: # Intentionally don't match pip's behavior here -- this is a temporary copy src_dir = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") else: @@ -96,33 +115,53 @@ def ensure_reqs(reqs): continue if isinstance(req, six.string_types): req = pkg_resources.Requirement.parse("{0}".format(str(req))) + # req = strip_extras_markers_from_requirement(req) new_reqs.append(req) return new_reqs -def _prepare_wheel_building_kwargs(ireq=None, src_root=None, editable=False): - # type: (Optional[InstallRequirement], Optional[str], bool) -> Dict[str, str] +def pep517_subprocess_runner(cmd, cwd=None, extra_environ=None): + # type: (List[str], Optional[str], Optional[Dict[str, str]]) -> None + """The default method of calling the wrapper subprocess.""" + env = os.environ.copy() + if extra_environ: + env.update(extra_environ) + + run(cmd, cwd=cwd, env=env, block=True, combine_stderr=True, return_object=False, + write_to_stdout=False, nospin=True) + + +def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, editable=False): + # type: (Optional[InstallRequirement], Optional[str], Optional[str], bool) -> Dict[str, str] download_dir = os.path.join(CACHE_DIR, "pkgs") # type: str mkdir_p(download_dir) wheel_download_dir = os.path.join(CACHE_DIR, "wheels") # type: str mkdir_p(wheel_download_dir) - if ireq is None: - src_dir = _get_src_dir(root=src_root) # type: str - elif ireq is not None and ireq.source_dir is not None: - src_dir = ireq.source_dir - elif ireq is not None and ireq.editable: - src_dir = _get_src_dir(root=src_root) - else: - src_dir = create_tracked_tempdir(prefix="reqlib-src") + if src_dir is None: + if editable and src_root is not None: + src_dir = src_root + elif ireq is None and src_root is not None: + src_dir = _get_src_dir(root=src_root) # type: str + elif ireq is not None and ireq.editable is not None and ireq.source_dir is not None: + src_dir = ireq.source_dir + elif ireq is not None and ireq.editable and src_root is not None: + src_dir = _get_src_dir(root=src_root) + else: + src_dir = create_tracked_tempdir(prefix="reqlib-src") # This logic matches pip's behavior, although I don't fully understand the # intention. I guess the idea is to build editables in-place, otherwise out # of the source tree? - if ireq is None and editable or (ireq is not None and ireq.editable): + if (ireq is not None and ireq.editable) or editable: build_dir = src_dir - else: + # else: + + # Let's always resolve in isolation + if src_dir is None: + src_dir = create_tracked_tempdir(prefix="reqlib-src") + if ireq is not None and not ireq.editable: build_dir = create_tracked_tempdir(prefix="reqlib-build") return { @@ -153,9 +192,12 @@ def iter_metadata(path, pkg_name=None, metadata_type="egg-info"): def find_egginfo(target, pkg_name=None): # type: (str, Optional[str]) -> Generator - egg_dirs = (egg_dir for egg_dir in iter_metadata(target, pkg_name=pkg_name)) + egg_dirs = ( + egg_dir for egg_dir in iter_metadata(target, pkg_name=pkg_name) + if egg_dir is not None + ) if pkg_name: - yield next(iter(egg_dirs), None) + yield next(iter(eggdir for eggdir in egg_dirs if eggdir is not None), None) else: for egg_dir in egg_dirs: yield egg_dir @@ -163,18 +205,29 @@ def find_egginfo(target, pkg_name=None): def find_distinfo(target, pkg_name=None): # type: (str, Optional[str]) -> Generator - dist_dirs = (dist_dir for dist_dir in iter_metadata(target, pkg_name=pkg_name, metadata_type="dist-info")) + dist_dirs = ( + dist_dir for dist_dir in iter_metadata(target, pkg_name=pkg_name, metadata_type="dist-info") + if dist_dir is not None + ) if pkg_name: - yield next(iter(dist_dirs), None) + yield next(iter(dist for dist in dist_dirs if dist is not None), None) else: for dist_dir in dist_dirs: yield dist_dir -def get_metadata(path, pkg_name=None): +def get_metadata(path, pkg_name=None, metadata_type=None): + # type: (str, Optional[str], Optional[str]) -> Dict[str, Union[str, List[RequirementType], Dict[str, RequirementType]]] + metadata_dirs = [] + wheel_allowed = metadata_type == "wheel" or metadata_type is None + egg_allowed = metadata_type == "egg" or metadata_type is None egg_dir = next(iter(find_egginfo(path, pkg_name=pkg_name)), None) dist_dir = next(iter(find_distinfo(path, pkg_name=pkg_name)), None) - matched_dir = next(iter(d for d in (dist_dir, egg_dir) if d is not None), None) + if dist_dir and wheel_allowed: + metadata_dirs.append(dist_dir) + if egg_dir and egg_allowed: + metadata_dirs.append(egg_dir) + matched_dir = next(iter(d for d in metadata_dirs if d is not None), None) metadata_dir = None base_dir = None if matched_dir is not None: @@ -184,50 +237,104 @@ def get_metadata(path, pkg_name=None): dist = None distinfo_dist = None egg_dist = None - if dist_dir is not None: + if wheel_allowed and dist_dir is not None: distinfo_dist = next(iter(pkg_resources.find_distributions(base_dir)), None) - if egg_dir is not None: + if egg_allowed and egg_dir is not None: path_metadata = pkg_resources.PathMetadata(base_dir, metadata_dir) egg_dist = next( iter(pkg_resources.distributions_from_metadata(path_metadata.egg_info)), None, ) dist = next(iter(d for d in (distinfo_dist, egg_dist) if d is not None), None) - if dist: - try: - requires = dist.requires() - except Exception: - requires = [] - try: - dep_map = dist._build_dep_map() - except Exception: - dep_map = {} - deps = [] - extras = {} - for k in dep_map.keys(): - if k is None: - deps.extend(dep_map.get(k)) - continue - else: - extra = None - _deps = dep_map.get(k) - if k.startswith(":python_version"): - marker = k.replace(":", "; ") - else: - marker = "" - extra = "{0}".format(k) - _deps = ["{0}{1}".format(str(req), marker) for req in _deps] - _deps = ensure_reqs(_deps) - if extra: - extras[extra] = _deps - else: - deps.extend(_deps) - return { - "name": dist.project_name, - "version": dist.version, - "requires": requires, - "extras": extras - } + if dist is not None: + return get_metadata_from_dist(dist) + return {} + + +def get_extra_name_from_marker(marker): + # type: (MarkerType) -> Optional[Text] + if not marker: + raise ValueError("Invalid value for marker: {0!r}".format(marker)) + if not getattr(marker, "_markers", None): + raise TypeError("Expecting a marker instance, received {0!r}".format(marker)) + for elem in marker._markers: + if isinstance(elem, tuple) and elem[0].value == "extra": + return elem[2].value + return None + + +def get_metadata_from_wheel(wheel_path): + # type: (Text) -> Dict[Any, Any] + if not isinstance(wheel_path, six.string_types): + raise TypeError("Expected string instance, received {0!r}".format(wheel_path)) + try: + dist = Wheel(wheel_path) + except Exception: + pass + metadata = dist.metadata + name = metadata.name + version = metadata.version + requires = [] + extras_keys = getattr(metadata, "extras", None) + extras = {} + for req in getattr(metadata, "run_requires", []): + parsed_req = init_requirement(req) + parsed_marker = parsed_req.marker + if parsed_marker: + extra = get_extra_name_from_marker(parsed_marker) + if extra is None: + requires.append(parsed_req) + continue + if extra not in extras: + extras[extra] = [] + parsed_req = strip_extras_markers_from_requirement(parsed_req) + extras[extra].append(parsed_req) + else: + requires.append(parsed_req) + return { + "name": name, + "version": version, + "requires": requires, + "extras": extras + } + + +def get_metadata_from_dist(dist): + # type: (Union[PathMetadata, DistInfoDistribution]) -> Dict[Text, Union[Text, List[RequirementType], Dict[Text, RequirementType]]] + try: + requires = dist.requires() + except Exception: + requires = [] + try: + dep_map = dist._build_dep_map() + except Exception: + dep_map = {} + deps = [] + extras = {} + for k in dep_map.keys(): + if k is None: + deps.extend(dep_map.get(k)) + continue + else: + extra = None + _deps = dep_map.get(k) + if k.startswith(":python_version"): + marker = k.replace(":", "; ") + else: + marker = "" + extra = "{0}".format(k) + _deps = ["{0}{1}".format(str(req), marker) for req in _deps] + _deps = ensure_reqs(_deps) + if extra: + extras[extra] = _deps + else: + deps.extend(_deps) + return { + "name": dist.project_name, + "version": dist.version, + "requires": requires, + "extras": extras + } @attr.s(slots=True, frozen=True) @@ -236,22 +343,27 @@ class BaseRequirement(object): requirement = attr.ib(default=None, cmp=True) # type: Optional[PkgResourcesRequirement] def __str__(self): + # type: () -> str return "{0}".format(str(self.requirement)) def as_dict(self): + # type: () -> Dict[str, Optional[PkgResourcesRequirement]] return {self.name: self.requirement} def as_tuple(self): + # type: () -> Tuple[str, Optional[PkgResourcesRequirement]] return (self.name, self.requirement) @classmethod def from_string(cls, line): + # type: (str) -> BaseRequirement line = line.strip() req = init_requirement(line) return cls.from_req(req) @classmethod def from_req(cls, req): + # type: (PkgResourcesRequirement) -> BaseRequirement name = None key = getattr(req, "key", None) name = getattr(req, "name", None) @@ -263,29 +375,52 @@ def from_req(cls, req): return cls(name=name, requirement=req) -@attr.s(slots=True, cmp=False) +@attr.s(slots=True, frozen=True) +class Extra(object): + name = attr.ib(type=str, default=None, cmp=True) + requirements = attr.ib(factory=frozenset, cmp=True, type=frozenset) + + def __str__(self): + # type: () -> str + return "{0}: {{{1}}}".format(self.section, ", ".join([r.name for r in self.requirements])) + + def add(self, req): + # type: (BaseRequirement) -> None + if req not in self.requirements: + return attr.evolve(self, requirements=frozenset(set(self.requirements).add(req))) + return self + + def as_dict(self): + # type: () -> Dict[str, Tuple[PkgResourcesRequirement]] + return {self.name: tuple([r.requirement for r in self.requirements])} + + +@attr.s(slots=True, cmp=True, hash=True) class SetupInfo(object): name = attr.ib(type=str, default=None, cmp=True) - base_dir = attr.ib(type=Path, default=None, cmp=True, hash=False) + base_dir = attr.ib(type=str, default=None, cmp=True, hash=False) version = attr.ib(type=str, default=None, cmp=True) - _requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) + _requirements = attr.ib(type=frozenset, factory=frozenset, cmp=True, hash=True) build_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) - build_backend = attr.ib(type=str, default="setuptools.build_meta", cmp=True) + build_backend = attr.ib(type=str, default="setuptools.build_meta:__legacy__", cmp=True) setup_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None, cmp=True) _extras_requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) setup_cfg = attr.ib(type=Path, default=None, cmp=True, hash=False) setup_py = attr.ib(type=Path, default=None, cmp=True, hash=False) pyproject = attr.ib(type=Path, default=None, cmp=True, hash=False) - ireq = attr.ib(default=None, cmp=True, hash=False) + ireq = attr.ib(default=None, cmp=True, hash=False) # type: Optional[InstallRequirement] extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict, cmp=False, hash=False) + metadata = attr.ib(default=None) # type: Optional[Tuple[str]] @property def requires(self): + # type: () -> Dict[str, RequirementType] return {req.name: req.requirement for req in self._requirements} @property def extras(self): + # type: () -> Dict[str, Dict[str, List[RequirementType]]] extras_dict = {} extras = set(self._extras_requirements) for section, deps in extras: @@ -315,9 +450,9 @@ def get_setup_cfg(cls, setup_cfg_path): results["name"] = parser.get("metadata", "name") if parser.has_option("metadata", "version"): results["version"] = parser.get("metadata", "version") - install_requires = () + install_requires = set() if parser.has_option("options", "install_requires"): - install_requires = tuple([ + install_requires = set([ BaseRequirement.from_string(dep) for dep in parser.get("options", "install_requires").split("\n") if dep @@ -325,6 +460,8 @@ def get_setup_cfg(cls, setup_cfg_path): results["install_requires"] = install_requires if parser.has_option("options", "python_requires"): results["python_requires"] = parser.get("options", "python_requires") + if parser.has_option("options", "build_requires"): + results["build_requires"] = parser.get("options", "build_requires") extras_require = () if "options.extras_require" in parser.sections(): extras_require = tuple([ @@ -341,14 +478,39 @@ def get_setup_cfg(cls, setup_cfg_path): results["extras_require"] = extras_require return results + @property + def egg_base(self): + base = None # type: Optional[Path] + if self.setup_py.exists(): + base = self.setup_py.parent + elif self.pyproject.exists(): + base = self.pyproject.parent + elif self.setup_cfg.exists(): + base = self.setup_cfg.parent + if base is None: + base = Path(self.base_dir) + if base is None: + base = Path(self.extra_kwargs["build_dir"]) + egg_base = base.joinpath("reqlib-metadata") + if not egg_base.exists(): + atexit.register(rmtree, egg_base.as_posix()) + egg_base.mkdir(parents=True, exist_ok=True) + return egg_base.as_posix() + def parse_setup_cfg(self): + # type: () -> None if self.setup_cfg is not None and self.setup_cfg.exists(): parsed = self.get_setup_cfg(self.setup_cfg.as_posix()) if self.name is None: self.name = parsed.get("name") if self.version is None: self.version = parsed.get("version") - self._requirements = self._requirements + parsed["install_requires"] + build_requires = parsed.get("build_requires", []) + if self.build_requires: + self.build_requires = tuple(set(self.build_requires) | set(build_requires)) + self._requirements = frozenset( + set(self._requirements) | parsed["install_requires"] + ) if self.python_requires is None: self.python_requires = parsed.get("python_requires") if not self._extras_requirements: @@ -362,14 +524,16 @@ def parse_setup_cfg(self): self._extras_requirements += ((extra, extras_tuple),) def run_setup(self): + # type: () -> None if self.setup_py is not None and self.setup_py.exists(): target_cwd = self.setup_py.parent.as_posix() - with cd(target_cwd), _suppress_distutils_logs(): + with temp_path(), cd(target_cwd), _suppress_distutils_logs(): # This is for you, Hynek # see https://github.com/hynek/environ_config/blob/69b1c8a/setup.py script_name = self.setup_py.as_posix() - args = ["egg_info"] + args = ["egg_info", "--egg-base", self.egg_base] g = {"__file__": script_name, "__name__": "__main__"} + sys.path.insert(0, os.path.dirname(os.path.abspath(script_name))) local_dict = {} if sys.version_info < (3, 5): save_argv = sys.argv @@ -390,9 +554,6 @@ def run_setup(self): python = os.environ.get('PIP_PYTHON_PATH', sys.executable) out, _ = run([python, "setup.py"] + args, cwd=target_cwd, block=True, combine_stderr=False, return_object=False, nospin=True) - except SystemExit: - print("Current directory: %s\nTarget file: %s\nDirectory Contents: %s\nSetup Path Contents: %s\n" % ( - os.getcwd(), script_name, os.listdir(os.getcwd()), os.listdir(os.path.dirname(script_name)))) finally: _setup_stop_after = None sys.argv = save_argv @@ -420,57 +581,160 @@ def run_setup(self): if not install_requires: install_requires = dist.install_requires if install_requires and not self.requires: - requirements = [init_requirement(req) for req in install_requires] + requirements = set([ + BaseRequirement.from_req(req) for req in install_requires + ]) if getattr(self.ireq, "extras", None): for extra in self.ireq.extras: - requirements.extend(list(self.extras.get(extra, []))) - self._requirements = self._requirements + tuple([ - BaseRequirement.from_req(req) for req in requirements - ]) + requirements |= set(list(self.extras.get(extra, []))) + self._requirements = frozenset( + set(self._requirements) | requirements + ) if dist.setup_requires and not self.setup_requires: self.setup_requires = tuple(dist.setup_requires) if not self.version: self.version = dist.get_version() - def get_egg_metadata(self): - if self.setup_py is not None and self.setup_py.exists(): - metadata = get_metadata(self.setup_py.parent.as_posix(), pkg_name=self.name) - if metadata: - if self.name is None: - self.name = metadata.get("name", self.name) - if not self.version: - self.version = metadata.get("version", self.version) - self._requirements = self._requirements + tuple([ - BaseRequirement.from_req(req) for req in metadata.get("requires", []) - ]) - if getattr(self.ireq, "extras", None): - for extra in self.ireq.extras: - extras = metadata.get("extras", {}).get(extra, []) - if extras: - extras_tuple = tuple([ - BaseRequirement.from_req(req) - for req in ensure_reqs(extras) - if req is not None - ]) - self._extras_requirements += ((extra, extras_tuple),) - self._requirements = self._requirements + extras_tuple + @contextlib.contextmanager + def run_pep517(self): + # type: (bool) -> Generator[pep517.wrappers.Pep517HookCaller, None, None] + with pep517.envbuild.BuildEnvironment(): + hookcaller = pep517.wrappers.Pep517HookCaller( + self.base_dir, self.build_backend + ) + hookcaller._subprocess_runner = pep517_subprocess_runner + build_deps = hookcaller.get_requires_for_build_wheel() + if self.ireq.editable: + build_deps += hookcaller.get_requires_for_build_sdist() + metadata_dirname = hookcaller.prepare_metadata_for_build_wheel(self.egg_base) + metadata_dir = os.path.join(self.egg_base, metadata_dirname) + try: + yield hookcaller + except Exception: + build_deps = ["setuptools", "wheel"] + self.build_requires = tuple(set(self.build_requires) | set(build_deps)) + + def build(self): + # type: () -> Optional[Text] + dist_path = None + with self.run_pep517() as hookcaller: + dist_path = self.build_pep517(hookcaller) + if os.path.exists(os.path.join(self.extra_kwargs["build_dir"], dist_path)): + self.get_metadata_from_wheel( + os.path.join(self.extra_kwargs["build_dir"], dist_path) + ) + if not self.metadata or not self.name: + self.get_egg_metadata() + else: + return dist_path + if not self.metadata or not self.name: + hookcaller._subprocess_runner( + ["setup.py", "egg_info", "--egg-base", self.egg_base] + ) + self.get_egg_metadata() + return dist_path + + def build_pep517(self, hookcaller): + # type: (pep517.wrappers.Pep517HookCaller) -> Optional[Text] + dist_path = None + try: + dist_path = hookcaller.build_wheel( + self.extra_kwargs["build_dir"], + metadata_directory=self.egg_base + ) + return dist_path + except Exception: + dist_path = hookcaller.build_sdist(self.extra_kwargs["build_dir"]) + self.get_egg_metadata(metadata_type="egg") + return dist_path + + def reload(self): + # type: () -> Dict[str, Any] + """ + Wipe existing distribution info metadata for rebuilding. + """ + for metadata_dir in os.listdir(self.egg_base): + shutil.rmtree(metadata_dir, ignore_errors=True) + self.metadata = None + self._requirements = frozenset() + self._extras_requirements = () + self.get_info() + + def get_metadata_from_wheel(self, wheel_path): + # type: (Text) -> Dict[Any, Any] + metadata_dict = get_metadata_from_wheel(wheel_path) + if metadata_dict: + self.populate_metadata(metadata_dict) + + def get_egg_metadata(self, metadata_dir=None, metadata_type=None): + # type: (Optional[str], Optional[str]) -> None + package_indicators = [self.pyproject, self.setup_py, self.setup_cfg] + # if self.setup_py is not None and self.setup_py.exists(): + metadata_dirs = [] + if any([fn is not None and fn.exists() for fn in package_indicators]): + metadata_dirs = [self.extra_kwargs["build_dir"], self.egg_base, self.extra_kwargs["src_dir"]] + if metadata_dir is not None: + metadata_dirs = [metadata_dir] + metadata_dirs + metadata = [ + get_metadata(d, pkg_name=self.name, metadata_type=metadata_type) + for d in metadata_dirs if os.path.exists(d) + ] + metadata = next(iter(d for d in metadata if d is not None), None) + if metadata is not None: + self.populate_metadata(metadata) + + def populate_metadata(self, metadata): + # type: (Dict[Any, Any]) -> None + self.metadata = tuple([(k, v) for k, v in metadata.items()]) + if self.name is None: + self.name = metadata.get("name", self.name) + if not self.version: + self.version = metadata.get("version", self.version) + self._requirements = frozenset( + set(self._requirements) | set([ + BaseRequirement.from_req(req) + for req in metadata.get("requires", []) + ]) + ) + if getattr(self.ireq, "extras", None): + for extra in self.ireq.extras: + extras = metadata.get("extras", {}).get(extra, []) + if extras: + extras_tuple = tuple([ + BaseRequirement.from_req(req) + for req in ensure_reqs(extras) + if req is not None + ]) + self._extras_requirements += ((extra, extras_tuple),) + self._requirements = frozenset( + set(self._requirements) | set(extras_tuple) + ) def run_pyproject(self): + # type: () -> None if self.pyproject and self.pyproject.exists(): result = get_pyproject(self.pyproject.parent) if result is not None: requires, backend = result if backend: self.build_backend = backend + else: + self.build_backend = "setuptools.build_meta:__legacy__" + self.build_requires = ("setuptools", "wheel") if requires and not self.build_requires: self.build_requires = tuple(requires) def get_info(self): - initial_path = os.path.abspath(os.getcwd()) + # type: () -> Dict[str, Any] if self.setup_cfg and self.setup_cfg.exists(): with cd(self.base_dir): self.parse_setup_cfg() - if self.setup_py and self.setup_py.exists(): + + with cd(self.base_dir), replaced_streams(): + self.run_pyproject() + self.build() + + if self.setup_py and self.setup_py.exists() and self.metadata is None: if not self.requires or not self.name: try: with cd(self.base_dir): @@ -478,16 +742,10 @@ def get_info(self): except Exception: with cd(self.base_dir): self.get_egg_metadata() - if not self.requires or not self.name: + if self.metadata is None or not self.name: with cd(self.base_dir): self.get_egg_metadata() - if self.pyproject and self.pyproject.exists(): - try: - with cd(self.base_dir): - self.run_pyproject() - finally: - os.chdir(initial_path) return self.as_dict() def as_dict(self): @@ -512,6 +770,7 @@ def as_dict(self): @classmethod def from_requirement(cls, requirement, finder=None): + # type: (TRequirement, PackageFinder) ireq = requirement.as_ireq() subdir = getattr(requirement.req, "subdirectory", None) return cls.from_ireq(ireq, subdir=subdir, finder=finder) @@ -527,8 +786,24 @@ def from_ireq(cls, ireq, subdir=None, finder=None): from .dependencies import get_finder finder = get_finder() + vcs_method, uri = split_vcs_method_from_uri(unquote(ireq.link.url_without_fragment)) + parsed = urlparse(uri) + if "file" in parsed.scheme: + url_path = parsed.path + if "@" in url_path: + url_path, _, _ = url_path.rpartition("@") + parsed = parsed._replace(path=url_path) + uri = urlunparse(parsed) + path = None + if ireq.link.scheme == "file" or uri.startswith("file://"): + if "file:/" in uri and "file:///" not in uri: + uri = uri.replace("file:/", "file:///") + path = pip_shims.shims.url_to_path(uri) + # if pip_shims.shims.is_installable_dir(path) and ireq.editable: + # ireq.source_dir = path kwargs = _prepare_wheel_building_kwargs(ireq) - ireq.populate_link(finder, False, False) + ireq.source_dir = kwargs["src_dir"] + # os.environ["PIP_BUILD_DIR"] = kwargs["build_dir"] ireq.ensure_has_source_dir(kwargs["build_dir"]) if not ( ireq.editable @@ -541,37 +816,31 @@ def from_ireq(cls, ireq, subdir=None, finder=None): else: only_download = False download_dir = kwargs["download_dir"] - ireq_src_dir = None - if ireq.link.scheme == "file": - path = pip_shims.shims.url_to_path(unquote(ireq.link.url_without_fragment)) - if pip_shims.shims.is_installable_dir(path): - ireq_src_dir = path - elif os.path.isdir(path): - raise RequirementError( - "The file URL points to a directory not installable: {}" - .format(ireq.link) - ) - - if not (ireq.editable and "file" in ireq.link.scheme): - pip_shims.shims.unpack_url( - ireq.link, - ireq.source_dir, - download_dir, - only_download=only_download, - session=finder.session, - hashes=ireq.hashes(False), - progress_bar="off", + elif path is not None and os.path.isdir(path): + raise RequirementError( + "The file URL points to a directory not installable: {}" + .format(ireq.link) ) - if ireq.editable: - created = cls.create( - ireq.source_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs - ) - else: + if not ireq.editable: build_dir = ireq.build_location(kwargs["build_dir"]) ireq._temp_build_dir.path = kwargs["build_dir"] - created = cls.create( - build_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs - ) + else: + build_dir = ireq.build_location(kwargs["src_dir"]) + ireq._temp_build_dir.path = kwargs["build_dir"] + + ireq.populate_link(finder, False, False) + pip_shims.shims.unpack_url( + ireq.link, + build_dir, + download_dir, + only_download=only_download, + session=finder.session, + hashes=ireq.hashes(False), + progress_bar="off", + ) + created = cls.create( + build_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs + ) return created @classmethod diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index bf04b354..10eda5a2 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -3,6 +3,8 @@ import io import os +import re +import string import sys from collections import defaultdict @@ -16,7 +18,10 @@ from first import first from packaging.markers import InvalidMarker, Marker, Op, Value, Variable from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet +from six.moves.urllib import parse as urllib_parse +from urllib3 import util as urllib3_util from vistir.misc import dedup +from vistir.path import is_valid_url from ..utils import SCHEME_LIST, VCS_LIST, is_star, add_ssh_scheme_to_git_uri @@ -24,18 +29,38 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Union, Optional, List, Set, Any, TypeVar, Tuple + from typing import Union, Optional, List, Set, Any, TypeVar, Tuple, Sequence, Dict, Text from attr import _ValidatorType + from packaging.requirements import Requirement as PackagingRequirement from pkg_resources import Requirement as PkgResourcesRequirement + from pkg_resources.extern.packaging.markers import Marker as PkgResourcesMarker from pip_shims.shims import Link + from vistir.compat import Path _T = TypeVar("_T") + TMarker = Union[Marker, PkgResourcesMarker] + TRequirement = Union[PackagingRequirement, PkgResourcesRequirement] HASH_STRING = " --hash={0}" +ALPHA_NUMERIC = r"[{0}{1}]".format(string.ascii_letters, string.digits) +PUNCTUATION = r"[\-_\.]" +ALPHANUM_PUNCTUATION = r"[{0}{1}\-_\.]".format(string.ascii_letters, string.digits) +NAME = r"{0}+{1}*{2}".format(ALPHANUM_PUNCTUATION, PUNCTUATION, ALPHA_NUMERIC) +REF = r"[{0}{1}\-\_\./]".format(string.ascii_letters, string.digits) +EXTRAS = r"(?P\[{0}(?:,{0})*\])".format(NAME) +NAME_WITH_EXTRAS = r"(?P{0}){1}?".format(NAME, EXTRAS) +NAME_RE = re.compile(NAME_WITH_EXTRAS) +SUBDIR_RE = r"(?:[&#]subdirectory=(?P.*))" +URL_NAME = r"(?:#egg={0})".format(NAME_WITH_EXTRAS) +REF_RE = r"(?:@(?P{0}+)?)".format(REF) +URL = r"(?P[^ ]+://)(?:(?P[^ ]+?\.?{0}+(?P:\d+)?))?(?P[:/])(?P[^ @]+){1}?".format(ALPHA_NUMERIC, REF_RE) +URL_RE = re.compile(r"{0}(?:{1}?{2}?)?".format(URL, URL_NAME, SUBDIR_RE)) +DIRECT_URL_RE = re.compile(r"{0}\s?@\s?{1}".format(NAME_WITH_EXTRAS, URL)) + def filter_none(k, v): - # type: (str, Any) -> bool + # type: (Text, Any) -> bool if v: return True return False @@ -47,7 +72,7 @@ def optional_instance_of(cls): def create_link(link): - # type: (str) -> Link + # type: (Text) -> Link if not isinstance(link, six.string_types): raise TypeError("must provide a string to instantiate a new link") @@ -55,8 +80,22 @@ def create_link(link): return Link(link) +def get_url_name(url): + # type: (Text) -> Text + """ + Given a url, derive an appropriate name to use in a pipfile. + + :param str url: A url to derive a string from + :returns: The name of the corresponding pipfile entry + :rtype: Text + """ + if not isinstance(url, six.string_types): + raise TypeError("Expected a string, got {0!r}".format(url)) + return urllib3_util.parse_url(url).host + + def init_requirement(name): - # type: (str) -> PkgResourcesRequirement + # type: (Text) -> TRequirement if not isinstance(name, six.string_types): raise TypeError("must supply a name to generate a requirement") @@ -70,18 +109,20 @@ def init_requirement(name): def extras_to_string(extras): + # type: (Sequence) -> Text """Turn a list of extras into a string""" if isinstance(extras, six.string_types): if extras.startswith("["): return extras - else: extras = [extras] - return "[{0}]".format(",".join(sorted(extras))) + if not extras: + return "" + return "[{0}]".format(",".join(sorted(set(extras)))) def parse_extras(extras_str): - # type: (str) -> List + # type: (Text) -> List """ Turn a string of extras into a parsed extras list """ @@ -92,7 +133,7 @@ def parse_extras(extras_str): def specs_to_string(specs): - # type: (List[str, Specifier]) -> str + # type: (List[Union[Text, Specifier]]) -> Text """ Turn a list of specifier tuples into a string """ @@ -109,53 +150,178 @@ def specs_to_string(specs): def build_vcs_uri( - vcs, # type: str - uri, # type: str - name=None, # type: Optional[str] - ref=None, # type: Optional[str] - subdirectory=None, # type: Optional[str] - extras=None # type: Optional[List[str]] + vcs, # type: Optional[Text] + uri, # type: Text + name=None, # type: Optional[Text] + ref=None, # type: Optional[Text] + subdirectory=None, # type: Optional[Text] + extras=None # type: Optional[List[Text]] ): - # type: (...) -> str + # type: (...) -> Text if extras is None: extras = [] - vcs_start = "{0}+".format(vcs) - if not uri.startswith(vcs_start): - uri = "{0}{1}".format(vcs_start, uri) + vcs_start = "" + if vcs is not None: + vcs_start = "{0}+".format(vcs) + if not uri.startswith(vcs_start): + uri = "{0}{1}".format(vcs_start, uri) if ref: uri = "{0}@{1}".format(uri, ref) if name: uri = "{0}#egg={1}".format(uri, name) if extras: - extras = extras_to_string(extras) - uri = "{0}{1}".format(uri, extras) + extras_string = extras_to_string(extras) + uri = "{0}{1}".format(uri, extras_string) if subdirectory: uri = "{0}&subdirectory={1}".format(uri, subdirectory) return uri +def convert_direct_url_to_url(direct_url): + # type: (Text) -> Text + """ + Given a direct url as defined by *PEP 508*, convert to a :class:`~pip_shims.shims.Link` + compatible URL by moving the name and extras into an **egg_fragment**. + + :param str direct_url: A pep-508 compliant direct url. + :return: A reformatted URL for use with Link objects and :class:`~pip_shims.shims.InstallRequirement` objects. + :rtype: Text + """ + direct_match = DIRECT_URL_RE.match(direct_url) + if direct_match is None: + url_match = URL_RE.match(direct_url) + if url_match or is_valid_url(direct_url): + return direct_url + match_dict = direct_match.groupdict() + if not match_dict: + raise ValueError("Failed converting value to normal URL, is it a direct URL? {0!r}".format(direct_url)) + url_segments = [match_dict.get(s) for s in ("scheme", "host", "path", "pathsep")] + url = "".join([s for s in url_segments if s is not None]) + new_url = build_vcs_uri( + None, + url, + ref=match_dict.get("ref"), + name=match_dict.get("name"), + extras=match_dict.get("extras"), + subdirectory=match_dict.get("subdirectory") + ) + return new_url + + +def convert_url_to_direct_url(url, name=None): + # type: (Text, Optional[Text]) -> Text + """ + Given a :class:`~pip_shims.shims.Link` compatible URL, convert to a direct url as + defined by *PEP 508* by extracting the name and extras from the **egg_fragment**. + + :param Text url: A :class:`~pip_shims.shims.InstallRequirement` compliant URL. + :param Optiona[Text] name: A name to use in case the supplied URL doesn't provide one. + :return: A pep-508 compliant direct url. + :rtype: Text + + :raises ValueError: Raised when the URL can't be parsed or a name can't be found. + :raises TypeError: When a non-string input is provided. + """ + if not isinstance(url, six.string_types): + raise TypeError( + "Expected a string to convert to a direct url, got {0!r}".format(url) + ) + direct_match = DIRECT_URL_RE.match(url) + if direct_match: + return url + url_match = URL_RE.match(url) + if url_match is None or not url_match.groupdict(): + raise ValueError("Failed parse a valid URL from {0!r}".format(url)) + match_dict = url_match.groupdict() + url_segments = [match_dict.get(s) for s in ("scheme", "host", "path", "pathsep")] + name = match_dict.get("name", name) + extras = match_dict.get("extras") + new_url = "" + if extras and not name: + url_segments.append(extras) + elif extras and name: + new_url = "{0}{1}@ ".format(name, extras) + else: + if name is not None: + new_url = "{0}@ ".format(name) + else: + raise ValueError( + "Failed to construct direct url: " + "No name could be parsed from {0!r}".format(url) + ) + if match_dict.get("ref"): + url_segments.append("@{0}".format(match_dict.get("ref"))) + url = "".join([s for s in url if s is not None]) + url = "{0}{1}".format(new_url, url) + return url + + def get_version(pipfile_entry): + # type: (Union[Text, Dict[Text, bool, List[Text]]]) -> Text if str(pipfile_entry) == "{}" or is_star(pipfile_entry): return "" elif hasattr(pipfile_entry, "keys") and "version" in pipfile_entry: if is_star(pipfile_entry.get("version")): return "" - return pipfile_entry.get("version", "") + return pipfile_entry.get("version", "").strip().lstrip("(").rstrip(")") if isinstance(pipfile_entry, six.string_types): - return pipfile_entry + return pipfile_entry.strip().lstrip("(").rstrip(")") return "" +def strip_extras_markers_from_requirement(req): + # type: (TRequirement) -> TRequirement + """ + Given a :class:`~packaging.requirements.Requirement` instance with markers defining + *extra == 'name'*, strip out the extras from the markers and return the cleaned + requirement + + :param PackagingRequirement req: A pacakaging requirement to clean + :return: A cleaned requirement + :rtype: PackagingRequirement + """ + if req is None: + raise TypeError("Must pass in a valid requirement, received {0!r}".format(req)) + if req.marker is not None: + req.marker._markers = _strip_extras_markers(req.marker._markers) + if not req.marker._markers: + req.marker = None + return req + + +def _strip_extras_markers(marker): + # type: (TMarker) -> TMarker + if marker is None or not isinstance(marker, (list, tuple)): + raise TypeError("Expecting a marker type, received {0!r}".format(marker)) + markers_to_remove = [] + # iterate forwards and generate a list of indexes to remove first, then reverse the + # list so we can remove the text that normally occurs after (but we will already + # be past it in the loop) + for i, marker_list in enumerate(marker): + if isinstance(marker_list, list): + cleaned = _strip_extras_markers(marker_list) + if not cleaned: + markers_to_remove.append(i) + elif isinstance(marker_list, tuple) and marker_list[0].value == "extra": + markers_to_remove.append(i) + for i in reversed(markers_to_remove): + del marker[i] + if i > 0 and marker[i - 1] == "and": + del marker[i - 1] + return marker + + def get_pyproject(path): + # type: (Union[Text, Path]) -> Tuple[List[Text], Text] """ Given a base path, look for the corresponding ``pyproject.toml`` file and return its build_requires and build_backend. - :param str path: The root path of the project, should be a directory (will be truncated) + :param Text path: The root path of the project, should be a directory (will be truncated) :return: A 2 tuple of build requirements and the build backend - :rtype: Tuple[List[str], str] + :rtype: Tuple[List[Text], Text] """ from vistir.compat import Path @@ -194,7 +360,7 @@ def get_pyproject(path): def split_markers_from_line(line): - # type: (str) -> Tuple[str, Optional[str]] + # type: (Text) -> Tuple[Text, Optional[Text]] """Split markers from a dependency""" if not any(line.startswith(uri_prefix) for uri_prefix in SCHEME_LIST): marker_sep = ";" @@ -208,7 +374,7 @@ def split_markers_from_line(line): def split_vcs_method_from_uri(uri): - # type: (str) -> Tuple[Optional[str], str] + # type: (Text) -> Tuple[Optional[Text], Text] """Split a vcs+uri formatted uri into (vcs, uri)""" vcs_start = "{0}+" vcs = first([vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))]) @@ -217,6 +383,27 @@ def split_vcs_method_from_uri(uri): return vcs, uri +def split_ref_from_uri(uri): + # type: (Text) -> Tuple[Text, Optional[Text]] + """ + Given a path or URI, check for a ref and split it from the path if it is present, + returning a tuple of the original input and the ref or None. + + :param Text uri: The path or URI to split + :returns: A 2-tuple of the path or URI and the ref + :rtype: Tuple[Text, Optional[Text]] + """ + if not isinstance(uri, six.string_types): + raise TypeError("Expected a string, received {0!r}".format(uri)) + parsed = urllib_parse.urlparse(uri) + path = parsed.path + ref = None + if "@" in path: + path, _, ref = path.rpartition("@") + parsed = parsed._replace(path=path) + return (urllib_parse.urlunparse(parsed), ref) + + def validate_vcs(instance, attr_, value): if value not in VCS_LIST: raise ValueError("Invalid vcs {0!r}".format(value)) @@ -597,12 +784,12 @@ def fix_requires_python_marker(requires_python): def normalize_name(pkg): - # type: (str) -> str + # type: (Text) -> Text """Given a package name, return its normalized, non-canonicalized form. - :param str pkg: The name of a package + :param Text pkg: The name of a package :return: A normalized package name - :rtype: str + :rtype: Text """ assert isinstance(pkg, six.string_types) @@ -610,12 +797,12 @@ def normalize_name(pkg): def get_name_variants(pkg): - # type: (str) -> Set[str] + # type: (Text) -> Set[Text] """ Given a packager name, get the variants of its name for both the canonicalized and "safe" forms. - :param str pkg: The package to lookup + :param Text pkg: The package to lookup :returns: A list of names. :rtype: Set """ diff --git a/src/requirementslib/utils.py b/src/requirementslib/utils.py index 1e16106a..a3ddfdde 100644 --- a/src/requirementslib/utils.py +++ b/src/requirementslib/utils.py @@ -10,17 +10,22 @@ import tomlkit import vistir -six.add_move(six.MovedAttribute("Mapping", "collections", "collections.abc")) -six.add_move(six.MovedAttribute("Sequence", "collections", "collections.abc")) -six.add_move(six.MovedAttribute("Set", "collections", "collections.abc")) -six.add_move(six.MovedAttribute("ItemsView", "collections", "collections.abc")) -from six.moves import Mapping, Sequence, Set, ItemsView +six.add_move(six.MovedAttribute("Mapping", "collections", "collections.abc")) # type: ignore # noqa +six.add_move(six.MovedAttribute("Sequence", "collections", "collections.abc")) # type: ignore # noqa +six.add_move(six.MovedAttribute("Set", "collections", "collections.abc")) # type: ignore # noqa +six.add_move(six.MovedAttribute("ItemsView", "collections", "collections.abc")) # type: ignore # noqa +from six.moves import Mapping, Sequence, Set, ItemsView # type: ignore # noqa from six.moves.urllib.parse import urlparse, urlsplit, urlunparse import pip_shims.shims from vistir.compat import Path from vistir.path import is_valid_url, ensure_mkdir_p, create_tracked_tempdir +from .environment import MYPY_RUNNING + +if MYPY_RUNNING: + from typing import Dict, Any, Optional, Union, Tuple, List, Iterable, Generator, Text + VCS_LIST = ("git", "svn", "hg", "bzr") @@ -69,11 +74,12 @@ def setup_logger(): def is_installable_dir(path): + # type: (Text) -> bool if pip_shims.shims.is_installable_dir(path): return True - path = Path(path) - pyproject = path.joinpath("pyproject.toml") - if pyproject.exists(): + pyproject_path = os.path.join(path, "pyproject.toml") + if os.path.exists(pyproject_path): + pyproject = Path(pyproject_path) pyproject_toml = tomlkit.loads(pyproject.read_text()) build_system = pyproject_toml.get("build-system", {}).get("build-backend", "") if build_system: @@ -82,7 +88,7 @@ def is_installable_dir(path): def strip_ssh_from_git_uri(uri): - # type: (str) -> str + # type: (Text) -> Text """Return git+ssh:// formatted URI to git+git@ format""" if isinstance(uri, six.string_types): if "git+ssh://" in uri: @@ -99,7 +105,7 @@ def strip_ssh_from_git_uri(uri): def add_ssh_scheme_to_git_uri(uri): - # type: (str) -> str + # type: (Text) -> Text """Cleans VCS uris from pip format""" if isinstance(uri, six.string_types): # Add scheme for parsing purposes, this is also what pip does @@ -114,6 +120,7 @@ def add_ssh_scheme_to_git_uri(uri): def is_vcs(pipfile_entry): + # type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool """Determine if dictionary entry from Pipfile is for a vcs dependency.""" if isinstance(pipfile_entry, Mapping): return any(key for key in pipfile_entry.keys() if key in VCS_LIST) @@ -128,6 +135,7 @@ def is_vcs(pipfile_entry): def is_editable(pipfile_entry): + # type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool if isinstance(pipfile_entry, Mapping): return pipfile_entry.get("editable", False) is True if isinstance(pipfile_entry, six.string_types): @@ -136,6 +144,7 @@ def is_editable(pipfile_entry): def multi_split(s, split): + # type: (Text, Iterable[Text]) -> List[Text] """Splits on multiple given separators.""" for r in split: s = s.replace(r, "|") @@ -143,13 +152,14 @@ def multi_split(s, split): def is_star(val): + # type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool return (isinstance(val, six.string_types) and val == "*") or ( isinstance(val, Mapping) and val.get("version", "") == "*" ) def convert_entry_to_path(path): - # type: (Dict[str, Any]) -> str + # type: (Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]) -> Text """Convert a pipfile entry to a string""" if not isinstance(path, Mapping): @@ -167,6 +177,7 @@ def convert_entry_to_path(path): def is_installable_file(path): + # type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool """Determine if a path can potentially be installed""" from packaging import specifiers @@ -187,7 +198,7 @@ def is_installable_file(path): parsed = urlparse(path) is_local = (not parsed.scheme or parsed.scheme == "file" or (len(parsed.scheme) == 1 and os.name == "nt")) if parsed.scheme and parsed.scheme == "file": - path = vistir.path.url_to_path(path) + path = vistir.compat.fs_decode(vistir.path.url_to_path(path)) normalized_path = vistir.path.normalize_path(path) if is_local and not os.path.exists(normalized_path): return False @@ -220,7 +231,7 @@ def get_dist_metadata(dist): def get_setup_paths(base_path, subdirectory=None): - # type: (str, Optional[str]) -> Dict[str, Optional[str]] + # type: (Text, Optional[Text]) -> Dict[Text, Optional[Text]] if base_path is None: raise TypeError("must provide a path to derive setup paths from") setup_py = os.path.join(base_path, "setup.py") @@ -245,23 +256,24 @@ def get_setup_paths(base_path, subdirectory=None): def prepare_pip_source_args(sources, pip_args=None): + # type: (List[Dict[Text, Union[Text, bool]]], Optional[List[Text]]) -> List[Text] if pip_args is None: pip_args = [] if sources: # Add the source to pip9. - pip_args.extend(["-i", sources[0]["url"]]) + pip_args.extend(["-i", sources[0]["url"]]) # type: ignore # Trust the host if it's not verified. if not sources[0].get("verify_ssl", True): - pip_args.extend(["--trusted-host", urlparse(sources[0]["url"]).hostname]) + pip_args.extend(["--trusted-host", urlparse(sources[0]["url"]).hostname]) # type: ignore # Add additional sources as extra indexes. if len(sources) > 1: for source in sources[1:]: - pip_args.extend(["--extra-index-url", source["url"]]) + pip_args.extend(["--extra-index-url", source["url"]]) # type: ignore # Trust the host if it's not verified. if not source.get("verify_ssl", True): pip_args.extend( ["--trusted-host", urlparse(source["url"]).hostname] - ) + ) # type: ignore return pip_args @@ -271,10 +283,11 @@ def _ensure_dir(path): @contextlib.contextmanager -def ensure_setup_py(base_dir): - if not base_dir: - base_dir = create_tracked_tempdir(prefix="requirementslib-setup") - base_dir = Path(base_dir) +def ensure_setup_py(base): + # type: (Text) -> Generator[None, None, None] + if not base: + base = create_tracked_tempdir(prefix="requirementslib-setup") + base_dir = Path(base) if base_dir.exists() and base_dir.name == "setup.py": base_dir = base_dir.parent elif not (base_dir.exists() and base_dir.is_dir()): From 865fdc6a6b98d2b217cc9278404f02bd90c404a9 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 13 Feb 2019 20:30:56 -0500 Subject: [PATCH 13/35] Update tests and remove old unused code Signed-off-by: Dan Ryan --- src/requirementslib/models/setup_info.py | 12 +----------- tests/unit/test_requirements.py | 6 +++--- tests/unit/test_setup_info.py | 4 +--- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 315b9a96..839e229e 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -144,25 +144,15 @@ def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, edita src_dir = src_root elif ireq is None and src_root is not None: src_dir = _get_src_dir(root=src_root) # type: str - elif ireq is not None and ireq.editable is not None and ireq.source_dir is not None: - src_dir = ireq.source_dir elif ireq is not None and ireq.editable and src_root is not None: src_dir = _get_src_dir(root=src_root) else: src_dir = create_tracked_tempdir(prefix="reqlib-src") - # This logic matches pip's behavior, although I don't fully understand the - # intention. I guess the idea is to build editables in-place, otherwise out - # of the source tree? - if (ireq is not None and ireq.editable) or editable: - build_dir = src_dir - # else: - # Let's always resolve in isolation if src_dir is None: src_dir = create_tracked_tempdir(prefix="reqlib-src") - if ireq is not None and not ireq.editable: - build_dir = create_tracked_tempdir(prefix="reqlib-build") + build_dir = create_tracked_tempdir(prefix="reqlib-build") return { "build_dir": build_dir, diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py index 0ed28c81..989b2668 100644 --- a/tests/unit/test_requirements.py +++ b/tests/unit/test_requirements.py @@ -239,8 +239,8 @@ def test_convert_from_pip_git_uri_normalize(monkeypatch): def test_get_requirements(monkeypatch): # Test eggs in URLs with monkeypatch.context() as m: - m.setattr(pip_shims.shims, "unpack_url", mock_unpack) - m.setattr(SetupInfo, "get_info", mock_run_requires) + # m.setattr(pip_shims.shims, "unpack_url", mock_unpack) + # m.setattr(SetupInfo, "get_info", mock_run_requires) url_with_egg = Requirement.from_line( 'https://github.com/IndustriaTech/django-user-clipboard/archive/0.6.1.zip#egg=django-user-clipboard' ).requirement @@ -336,7 +336,7 @@ def test_local_editable_ref(monkeypatch): def test_pep_508(): r = Requirement.from_line("tablib@ https://github.com/kennethreitz/tablib/archive/v0.12.1.zip") assert r.specifiers == "==0.12.1" - assert r.req.link.url == "https://github.com/kennethreitz/tablib/archive/v0.12.1.zip" + assert r.req.link.url == "https://github.com/kennethreitz/tablib/archive/v0.12.1.zip#egg=tablib" assert r.req.req.name == "tablib" assert r.req.req.url == "https://github.com/kennethreitz/tablib/archive/v0.12.1.zip" requires, setup_requires, build_requires = r.req.dependencies diff --git a/tests/unit/test_setup_info.py b/tests/unit/test_setup_info.py index 6ef1c210..bf061b89 100644 --- a/tests/unit/test_setup_info.py +++ b/tests/unit/test_setup_info.py @@ -34,8 +34,6 @@ def test_remote_req(url_line, name, requires): r = Requirement.from_line(url_line) assert r.name == name setup_dict = r.req.setup_info.as_dict() - if "typing" in requires and not sys.version_info < (3, 5): - requires.remove("typing") assert sorted(list(setup_dict.get("requires").keys())) == sorted(requires) @@ -44,7 +42,7 @@ def test_no_duplicate_egg_info(): base_dir = vistir.compat.Path(os.path.abspath(os.getcwd())).as_posix() r = Requirement.from_line("-e {}".format(base_dir)) egg_info_name = "{}.egg-info".format(r.name.replace("-", "_")) - assert os.path.isdir(os.path.join(base_dir, "src", egg_info_name)) + assert os.path.isdir(os.path.join(base_dir, "reqlib-metadata", egg_info_name)) assert not os.path.isdir(os.path.join(base_dir, egg_info_name)) From 47fe8b102842dcde26e25f787826f1cee8e578e0 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Thu, 14 Feb 2019 00:09:56 -0500 Subject: [PATCH 14/35] Delay parsing of parsed line on VCS Requirement until after instantiation Signed-off-by: Dan Ryan --- src/requirementslib/models/old_file.py | 2443 -------------------- src/requirementslib/models/requirements.py | 87 +- src/requirementslib/models/setup_info.py | 77 +- src/requirementslib/models/utils.py | 14 +- tasks/__init__.py | 54 +- tests/unit/test_setup_info.py | 21 +- 6 files changed, 128 insertions(+), 2568 deletions(-) delete mode 100644 src/requirementslib/models/old_file.py diff --git a/src/requirementslib/models/old_file.py b/src/requirementslib/models/old_file.py deleted file mode 100644 index 845d83df..00000000 --- a/src/requirementslib/models/old_file.py +++ /dev/null @@ -1,2443 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import absolute_import, print_function - -import collections -import copy -import hashlib -import os - -from contextlib import contextmanager -from functools import partial - -import attr -import pep517 -import pep517.wrappers -import pip_shims -import vistir - -from first import first -from packaging.markers import Marker -from packaging.requirements import Requirement as PackagingRequirement -from packaging.specifiers import Specifier, SpecifierSet, LegacySpecifier, InvalidSpecifier -from packaging.utils import canonicalize_name -from six.moves.urllib import parse as urllib_parse -from six.moves.urllib.parse import unquote -from vistir.compat import Path -from vistir.misc import dedup -from vistir.path import ( - create_tracked_tempdir, - get_converted_relative_path, - is_file_url, - is_valid_url, - normalize_path, - mkdir_p -) - -from ..exceptions import RequirementError -from ..utils import ( - VCS_LIST, - is_installable_file, - is_vcs, - ensure_setup_py, - add_ssh_scheme_to_git_uri, - strip_ssh_from_git_uri, - get_setup_paths -) -from .setup_info import SetupInfo, _prepare_wheel_building_kwargs -from .utils import ( - HASH_STRING, - build_vcs_uri, - extras_to_string, - filter_none, - format_requirement, - get_version, - init_requirement, - is_pinned_requirement, - make_install_requirement, - parse_extras, - specs_to_string, - split_markers_from_line, - split_vcs_method_from_uri, - validate_path, - validate_specifiers, - validate_vcs, - normalize_name, - create_link, - get_pyproject, -) - -from ..environment import MYPY_RUNNING - -if MYPY_RUNNING: - from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, NoReturn - from pip_shims.shims import Link, InstallRequirement - RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) - from six.moves.urllib.parse import SplitResult - from .vcs import VCSRepository - - -SPECIFIERS_BY_LENGTH = sorted(list(Specifier._operators.keys()), key=len, reverse=True) - - -run = partial(vistir.misc.run, combine_stderr=False, return_object=True, nospin=True) - - -class Line(object): - def __init__(self, line): - # type: (str) -> None - self.editable = line.startswith("-e ") - if self.editable: - line = line[len("-e "):] - self.line = line - self.hashes = [] # type: List[str] - self.extras = [] # type: List[str] - self.markers = None # type: Optional[str] - self.vcs = None # type: Optional[str] - self.path = None # type: Optional[str] - self.relpath = None # type: Optional[str] - self.uri = None # type: Optional[str] - self._link = None # type: Optional[Link] - self.is_local = False - self.name = None # type: Optional[str] - self.specifier = None # type: Optional[str] - self.parsed_marker = None # type: Optional[Marker] - self.preferred_scheme = None # type: Optional[str] - self.requirement = None # type: Optional[PackagingRequirement] - self.is_direct_url = False # type: bool - self._parsed_url = None # type: Optional[urllib_parse.ParseResult] - self._setup_cfg = None # type: Optional[str] - self._setup_py = None # type: Optional[str] - self._pyproject_toml = None # type: Optional[str] - self._pyproject_requires = None # type: Optional[List[str]] - self._pyproject_backend = None # type: Optional[str] - self._wheel_kwargs = None # type: Dict[str, str] - self._vcsrepo = None # type: Optional[VCSRepository] - self._setup_info = None # type: Optional[SetupInfo] - self._ref = None # type: Optional[str] - self._ireq = None # type: Optional[InstallRequirement] - self._src_root = None # type: Optional[str] - self.dist = None # type: Any - super(Line, self).__init__() - self.parse() - - def __hash__(self): - return hash(( - self.editable, self.line, self.markers, tuple(self.extras), - tuple(self.hashes), self.vcs, self.ireq) - ) - - @classmethod - def split_hashes(cls, line): - # type: (str) -> Tuple[str, List[str]] - if "--hash" not in line: - return line, [] - split_line = line.split() - line_parts = [] # type: List[str] - hashes = [] # type: List[str] - for part in split_line: - if part.startswith("--hash"): - param, _, value = part.partition("=") - hashes.append(value) - else: - line_parts.append(part) - line = " ".join(line_parts) - return line, hashes - - @property - def line_with_prefix(self): - # type: () -> str - line = self.line - if self.is_direct_url: - line = self.link.url - if self.editable: - return "-e {0}".format(line) - return line - - @property - def base_path(self): - # type: () -> Optional[str] - if not self.link and not self.path: - self.parse_link() - if not self.path: - pass - path = normalize_path(self.path) - if os.path.exists(path) and os.path.isdir(path): - path = path - elif os.path.exists(path) and os.path.isfile(path): - path = os.path.dirname(path) - else: - path = None - return path - - @property - def setup_py(self): - # type: () -> Optional[str] - if self._setup_py is None: - self.populate_setup_paths() - return self._setup_py - - @property - def setup_cfg(self): - # type: () -> Optional[str] - if self._setup_cfg is None: - self.populate_setup_paths() - return self._setup_cfg - - @property - def pyproject_toml(self): - # type: () -> Optional[str] - if self._pyproject_toml is None: - self.populate_setup_paths() - return self._pyproject_toml - - @property - def specifiers(self): - # type: () -> Optional[SpecifierSet] - if self.ireq is not None and self.ireq.req is not None: - return self.ireq.req.specifier - elif self.requirement is not None: - return self.requirement.specifier - return None - - @specifiers.setter - def specifiers(self, specifiers): - # type: (Union[str, SpecifierSet]) -> None - if type(specifiers) is not SpecifierSet: - if type(specifiers) in six.string_types: - specifiers = SpecifierSet(specifiers) - else: - raise TypeError("Must pass a string or a SpecifierSet") - if self.ireq is not None and self.ireq.req is not None: - self._ireq.req.specifier = specifiers - if self.requirement is not None: - self.requirement.specifier = specifiers - - def populate_setup_paths(self): - # type: () -> None - if not self.link and not self.path: - self.parse_link() - if not self.path: - return - base_path = self.base_path - if base_path is None: - return - setup_paths = get_setup_paths(self.base_path, subdirectory=self.subdirectory) # type: Dict[str, Optional[str]] - self._setup_py = setup_paths.get("setup_py") - self._setup_cfg = setup_paths.get("setup_cfg") - self._pyproject_toml = setup_paths.get("pyproject_toml") - - @property - def pyproject_requires(self): - # type: () -> Optional[List[str]] - if self._pyproject_requires is None and self.pyproject_toml is not None: - pyproject_requires, pyproject_backend = get_pyproject(self.path) - self._pyproject_requires = pyproject_requires - self._pyproject_backend = pyproject_backend - return self._pyproject_requires - - @property - def pyproject_backend(self): - # type: () -> Optional[str] - if self._pyproject_requires is None and self.pyproject_toml is not None: - pyproject_requires, pyproject_backend = get_pyproject(self.path) - if not pyproject_backend and self.setup_cfg is not None: - setup_dict = SetupInfo.get_setup_cfg(self.setup_cfg) - pyproject_backend = "setuptools.build_meta" - pyproject_requires = setup_dict.get("build_requires", ["setuptools", "wheel"]) - - self._pyproject_requires = pyproject_requires - self._pyproject_backend = pyproject_backend - return self._pyproject_backend - - def parse_hashes(self): - # type: () -> None - """ - Parse hashes from *self.line* and set them on the current object. - :returns: Nothing - :rtype: None - """ - - line, hashes = self.split_hashes(self.line) - self.hashes = hashes - self.line = line - - def parse_extras(self): - # type: () -> None - """ - Parse extras from *self.line* and set them on the current object - :returns: Nothing - :rtype: None - """ - - extras = None - if "@" in self.line: - parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(self.line)) - if not parsed.scheme: - name, _, line = self.line.partition("@") - name = name.strip() - line = line.strip() - if is_vcs(line) or is_valid_url(line): - self.is_direct_url = True - name, extras = pip_shims.shims._strip_extras(name) - self.name = name - self.line = line - else: - self.line, extras = pip_shims.shims._strip_extras(self.line) - else: - self.line, extras = pip_shims.shims._strip_extras(self.line) - if extras is not None: - self.extras = parse_extras(extras) - - def get_url(self): - # type: () -> str - """Sets ``self.name`` if given a **PEP-508** style URL""" - - line = self.line - if self.vcs is not None and self.line.startswith("{0}+".format(self.vcs)): - _, _, _parseable = self.line.partition("+") - parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(_parseable)) - else: - parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(line)) - if "@" in self.line and parsed.scheme == "": - name, _, url = self.line.partition("@") - if self.name is None: - self.name = name - if is_valid_url(url): - self.is_direct_url = True - line = url.strip() - parsed = urllib_parse.urlparse(line) - self._parsed_url = parsed - return line - - @property - def url(self): - # type: () -> Optional[str] - if self.uri is not None: - url = add_ssh_scheme_to_git_uri(self.uri) - url = getattr(self.link, "url_without_fragment", None) - if url is not None: - url = add_ssh_scheme_to_git_uri(unquote(url)) - if url is not None and self._parsed_url is None: - if self.vcs is not None: - _, _, _parseable = url.partition("+") - self._parsed_url = urllib_parse.urlparse(_parseable) - return url - - @property - def link(self): - # type: () -> Link - if self._link is None: - self.parse_link() - return self._link - - @property - def subdirectory(self): - # type: () -> Optional[str] - if self.link is not None: - return self.link.subdirectory_fragment - return "" - - @property - def is_wheel(self): - # type: () -> bool - if self.link is None: - return False - return self.link.is_wheel - - @property - def is_artifact(self): - # type: () -> bool - if self.link is None: - return False - return self.link.is_artifact - - @property - def is_vcs(self): - # type: () -> bool - # Installable local files and installable non-vcs urls are handled - # as files, generally speaking - if is_vcs(self.line) or is_vcs(self.get_url()): - return True - return False - - @property - def is_url(self): - # type: () -> bool - url = self.get_url() - if (is_valid_url(url) or is_file_url(url)): - return True - return False - - @property - def is_path(self): - # type: () -> bool - if self.path and ( - self.path.startswith(".") or os.path.isabs(self.path) or - os.path.exists(self.path) - ): - return True - elif os.path.exists(self.line) or os.path.exists(self.get_url()): - return True - return False - - @property - def is_file(self): - # type: () -> bool - if self.is_path or is_file_url(self.get_url()) or (self._parsed_url and self._parsed_url.scheme == "file"): - return True - return False - - @property - def is_named(self): - # type: () -> bool - return not (self.is_file or self.is_url or self.is_vcs) - - @property - def ref(self): - # type: () -> Optional[str] - if self._ref is None: - if self.relpath and "@" in self.relpath: - self._relpath, _, self._ref = self.relpath.rpartition("@") - return self._ref - - @property - def ireq(self): - # type: () -> Optional[pip_shims.InstallRequirement] - if self._ireq is None: - self.parse_ireq() - return self._ireq - - @property - def is_installable(self): - # type: () -> bool - if is_installable_file(self.line) or is_installable_file(self.get_url()) or is_installable_file(self.path) or is_installable_file(self.base_path): - return True - return False - - @property - def setup_info(self): - # type: () -> Optional[SetupInfo] - if self._setup_info is None and not self.is_named: - self._setup_info = SetupInfo.from_ireq(self.ireq) - self._setup_info.get_info() - return self._setup_info - - def _get_vcsrepo(self): - # type: () -> Optional[VCSRepository] - from .vcs import VCSRepository - checkout_directory = self.wheel_kwargs["src_dir"] # type: ignore - if self.name is not None: - checkout_directory = os.path.join(checkout_directory, self.name) # type: ignore - vcsrepo = VCSRepository( - url=self.link.url, - name=self.name, - ref=self.ref if self.ref else None, - checkout_directory=checkout_directory, - vcs_type=self.vcs, - subdirectory=self.subdirectory, - ) - if not self.link.scheme.startswith("file"): - vcsrepo.obtain() - return vcsrepo - - @property - def vcsrepo(self): - # type: () -> Optional[VCSRepository] - if self._vcsrepo is None: - self._vcsrepo = self._get_vcsrepo() - return self._vcsrepo - - def get_ireq(self): - # type: () -> InstallRequirement - if self.is_named: - ireq = pip_shims.shims.install_req_from_line(self.line) - elif (self.is_file or self.is_url) and not self.is_vcs: - line = self.line - if self.is_direct_url: - line = self.link.url - scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" - local_line = next(iter([ - os.path.dirname(os.path.abspath(f)) for f in [ - self.setup_py, self.setup_cfg, self.pyproject_toml - ] if f is not None - ]), None) - line = local_line if local_line is not None else self.line - if scheme == "path": - if not line and self.base_path is not None: - line = os.path.abspath(self.base_path) - else: - if self.link is not None: - line = self.link.url_without_fragment - else: - if self.uri is not None: - line = self.uri - else: - line = self.path - if self.editable: - ireq = pip_shims.shims.install_req_from_editable(self.link.url) - else: - ireq = pip_shims.shims.install_req_from_line(line) - else: - if self.editable: - ireq = pip_shims.shims.install_req_from_editable(self.link.url) - else: - ireq = pip_shims.shims.install_req_from_line(self.link.url) - if self.extras and not ireq.extras: - ireq.extras = set(self.extras) - if self.parsed_marker is not None and not ireq.markers: - ireq.markers = self.parsed_marker - if not ireq.req and self.requirement is not None: - ireq.req = PackagingRequirement(str(self.requirement)) - return ireq - - def parse_ireq(self): - # type: () -> None - if self._ireq is None: - self._ireq = self.get_ireq() - if self._ireq is not None: - if self.requirement is not None and self._ireq.req is None: - self._ireq.req = self.requirement - - def _parse_wheel(self): - # type: () -> Optional[str] - if not self.is_wheel: - pass - from pip_shims.shims import Wheel - _wheel = Wheel(self.link.filename) - name = _wheel.name - version = _wheel.version - self.specifier = "=={0}".format(version) - return name - - def _parse_name_from_link(self): - # type: () -> Optional[str] - - if self.link is None: - return None - if getattr(self.link, "egg_fragment", None): - return self.link.egg_fragment - elif self.is_wheel: - return self._parse_wheel() - return None - - def _parse_name_from_line(self): - # type: () -> Optional[str] - - if not self.is_named: - pass - name = self.line - specifier_match = next( - iter(spec for spec in SPECIFIERS_BY_LENGTH if spec in self.line), None - ) - if specifier_match is not None: - name, specifier_match, version = name.partition(specifier_match) - self.specifier = "{0}{1}".format(specifier_match, version) - return name - - def parse_name(self): - # type: () -> None - if self.name is None: - name = None - if self.link is not None: - name = self._parse_name_from_link() - if name is None and ( - (self.is_url or self.is_artifact or self.is_vcs) and self._parsed_url - ): - if self._parsed_url.fragment: - _, _, name = self._parsed_url.fragment.partition("egg=") - if "&" in name: - # subdirectory fragments might also be in here - name, _, _ = name.partition("&") - if self.is_named and name is None: - name = self._parse_name_from_line() - if name is not None: - name, extras = pip_shims.shims._strip_extras(name) - if extras is not None and not self.extras: - self.extras = parse_extras(extras) - self.name = name - - def _parse_requirement_from_vcs(self): - # type: () -> Optional[PackagingRequirement] - name = self.name if self.name else self.link.egg_fragment - url = self.uri if self.uri else unquote(self.link.url) - if self.is_direct_url: - url = self.link.url - if not name: - raise ValueError( - "pipenv requires an #egg fragment for version controlled " - "dependencies. Please install remote dependency " - "in the form {0}#egg=.".format(url) - ) - req = init_requirement(canonicalize_name(name)) # type: PackagingRequirement - req.editable = self.editable - if not getattr(req, "url") and self.link: - req.url = url - req.line = self.link.url - if ( - self.uri != unquote(self.link.url_without_fragment) - and "git+ssh://" in self.link.url - and (self.uri is not None and "git+git@" in self.uri) - ): - req.line = self.uri - req.url = self.uri - if self.ref: - if self._vcsrepo is not None: - req.revision = self._vcsrepo.get_commit_hash() - else: - req.revision = self.ref - if self.extras: - req.extras = self.extras - req.vcs = self.vcs - req.link = self.link - if self.path and self.link and self.link.scheme.startswith("file"): - req.local_file = True - req.path = self.path - return req - - def parse_requirement(self): - # type: () -> None - if self.name is None: - self.parse_name() - if self.is_named: - self.requirement = init_requirement(self.line) - elif self.is_vcs: - self.requirement = self._parse_requirement_from_vcs() - if self.name is None and ( - self.requirement is not None and self.requirement.name is not None - ): - self.name = self.requirement.name - if self.name is not None and self.requirement is None: - self.requirement = init_requirement(self.name) - if self.requirement: - if self.parsed_marker is not None: - self.requirement.marker = self.parsed_marker - if self.is_url or self.is_file and (self.link or self.url) and not self.is_vcs: - if self.uri: - self.requirement.url = self.uri - elif self.link: - self.requirement.url = unquote(self.link.url_without_fragment) - else: - self.requirement.url = self.url - if self.extras and not self.requirement.extras: - self.requirement.extras = set(self.extras) - - def parse_link(self): - # type: () -> None - if self.is_file or self.is_url or self.is_vcs: - vcs, prefer, relpath, path, uri, link = FileRequirement.get_link_from_line(self.line) - ref = None - if link is not None and "@" in link.path and uri is not None: - uri, _, ref = uri.rpartition("@") - if relpath is not None and "@" in relpath: - relpath, _, ref = relpath.rpartition("@") - self._ref = ref - self.vcs = vcs - self.preferred_scheme = prefer - self.relpath = relpath - self.path = path - self.uri = uri - if self.is_direct_url and self.name is not None and vcs is not None: - self._link = create_link( - build_vcs_uri(vcs=vcs, uri=uri, ref=ref, extras=self.extras, name=self.name) - ) - else: - self._link = link - - def parse_markers(self): - # type: () -> None - if self.markers: - markers = PackagingRequirement("fakepkg; {0}".format(self.markers)).marker - self.parsed_marker = markers - - def parse(self): - # type: () -> None - self.parse_hashes() - self.line, self.markers = split_markers_from_line(self.line) - self.parse_extras() - self.line = self.line.strip('"').strip("'").strip() - if self.line.startswith("git+file:/") and not self.line.startswith("git+file:///"): - self.line = self.line.replace("git+file:/", "git+file:///") - self.parse_markers() - if self.is_file: - self.populate_setup_paths() - self.parse_link() - self.parse_requirement() - self.parse_ireq() - - -@attr.s(slots=True, hash=True) -class NamedRequirement(object): - name = attr.ib() # type: str - version = attr.ib(validator=attr.validators.optional(validate_specifiers)) # type: Optional[str] - req = attr.ib() # type: PackagingRequirement - extras = attr.ib(default=attr.Factory(list)) # type: Tuple[str] - editable = attr.ib(default=False) # type: bool - _parsed_line = attr.ib(default=None) # type: Optional[Line] - - @req.default - def get_requirement(self): - # type: () -> RequirementType - req = init_requirement( - "{0}{1}".format(canonicalize_name(self.name), self.version) - ) - return req - - @property - def parsed_line(self): - # type: () -> Optional[Line] - if self._parsed_line is None: - self._parsed_line = Line(self.line_part) - return self._parsed_line - - @classmethod - def from_line(cls, line, parsed_line=None): - # type: (str, Optional[Line]) -> NamedRequirement - req = init_requirement(line) - specifiers = None # type: Optional[str] - if req.specifier: - specifiers = specs_to_string(req.specifier) - req.line = line - name = getattr(req, "name", None) - if not name: - name = getattr(req, "project_name", None) - req.name = name - if not name: - name = getattr(req, "key", line) - req.name = name - creation_kwargs = { - "name": name, - "version": specifiers, - "req": req, - "parsed_line": parsed_line, - "extras": None - } - extras = None # type: Optional[Tuple[str]] - if req.extras: - extras = list(req.extras) - creation_kwargs["extras"] = extras - return cls(**creation_kwargs) - - @classmethod - def from_pipfile(cls, name, pipfile): - # type: (str, Dict[str, Union[str, Optional[str], Optional[List[str]]]]) -> NamedRequirement - creation_args = {} # type: Dict[str, Union[Optional[str], Optional[List[str]]]] - if hasattr(pipfile, "keys"): - attr_fields = [field.name for field in attr.fields(cls)] - creation_args = {k: v for k, v in pipfile.items() if k in attr_fields} - creation_args["name"] = name - version = get_version(pipfile) # type: Optional[str] - extras = creation_args.get("extras", None) - creation_args["version"] = version - req = init_requirement("{0}{1}".format(name, version)) - if extras: - req.extras += tuple(extras) - creation_args["req"] = req - return cls(**creation_args) # type: ignore - - @property - def line_part(self): - # type: () -> str - # FIXME: This should actually be canonicalized but for now we have to - # simply lowercase it and replace underscores, since full canonicalization - # also replaces dots and that doesn't actually work when querying the index - return "{0}".format(normalize_name(self.name)) - - @property - def pipfile_part(self): - # type: () -> Dict[str, Any] - pipfile_dict = attr.asdict(self, filter=filter_none).copy() # type: ignore - if "version" not in pipfile_dict: - pipfile_dict["version"] = "*" - if "_parsed_line" in pipfile_dict: - pipfile_dict.pop("_parsed_line") - name = pipfile_dict.pop("name") - return {name: pipfile_dict} - - -LinkInfo = collections.namedtuple( - "LinkInfo", ["vcs_type", "prefer", "relpath", "path", "uri", "link"] -) - - -@attr.s(slots=True, cmp=True) -class FileRequirement(object): - """File requirements for tar.gz installable files or wheels or setup.py - containing directories.""" - - #: Path to the relevant `setup.py` location - setup_path = attr.ib(default=None, cmp=True) # type: Optional[str] - #: path to hit - without any of the VCS prefixes (like git+ / http+ / etc) - path = attr.ib(default=None, cmp=True) # type: Optional[str] - #: Whether the package is editable - editable = attr.ib(default=False, cmp=True) # type: bool - #: Extras if applicable - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[str] - _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[str] - #: URI of the package - uri = attr.ib(cmp=True) # type: Optional[str] - #: Link object representing the package to clone - link = attr.ib(cmp=True) # type: Optional[Link] - #: PyProject Requirements - pyproject_requires = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple - #: PyProject Build System - pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[str] - #: PyProject Path - pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[str] - #: Setup metadata e.g. dependencies - _setup_info = attr.ib(default=None, cmp=True) # type: Optional[SetupInfo] - _has_hashed_name = attr.ib(default=False, cmp=True) # type: bool - _parsed_line = attr.ib(default=None, hash=True) # type: Optional[Line] - #: Package name - name = attr.ib(cmp=True) # type: Optional[str] - #: A :class:`~pkg_resources.Requirement` isntance - req = attr.ib(cmp=True) # type: Optional[PackagingRequirement] - - @classmethod - def get_link_from_line(cls, line): - # type: (str) -> LinkInfo - """Parse link information from given requirement line. - - Return a 6-tuple: - - - `vcs_type` indicates the VCS to use (e.g. "git"), or None. - - `prefer` is either "file", "path" or "uri", indicating how the - information should be used in later stages. - - `relpath` is the relative path to use when recording the dependency, - instead of the absolute path/URI used to perform installation. - This can be None (to prefer the absolute path or URI). - - `path` is the absolute file path to the package. This will always use - forward slashes. Can be None if the line is a remote URI. - - `uri` is the absolute URI to the package. Can be None if the line is - not a URI. - - `link` is an instance of :class:`pip._internal.index.Link`, - representing a URI parse result based on the value of `uri`. - - This function is provided to deal with edge cases concerning URIs - without a valid netloc. Those URIs are problematic to a straight - ``urlsplit` call because they cannot be reliably reconstructed with - ``urlunsplit`` due to a bug in the standard library: - - >>> from urllib.parse import urlsplit, urlunsplit - >>> urlunsplit(urlsplit('git+file:///this/breaks')) - 'git+file:/this/breaks' - >>> urlunsplit(urlsplit('file:///this/works')) - 'file:///this/works' - - See `https://bugs.python.org/issue23505#msg277350`. - """ - - # Git allows `git@github.com...` lines that are not really URIs. - # Add "ssh://" so we can parse correctly, and restore afterwards. - fixed_line = add_ssh_scheme_to_git_uri(line) # type: str - added_ssh_scheme = fixed_line != line # type: bool - - # We can assume a lot of things if this is a local filesystem path. - if "://" not in fixed_line: - p = Path(fixed_line).absolute() # type: Path - path = p.as_posix() # type: Optional[str] - uri = p.as_uri() # type: str - link = create_link(uri) # type: Link - relpath = None # type: Optional[str] - try: - relpath = get_converted_relative_path(path) - except ValueError: - relpath = None - return LinkInfo(None, "path", relpath, path, uri, link) - - # This is an URI. We'll need to perform some elaborated parsing. - - parsed_url = urllib_parse.urlsplit(fixed_line) # type: SplitResult - original_url = parsed_url._replace() # type: SplitResult - - # Split the VCS part out if needed. - original_scheme = parsed_url.scheme # type: str - vcs_type = None # type: Optional[str] - if "+" in original_scheme: - scheme = None # type: Optional[str] - vcs_type, _, scheme = original_scheme.partition("+") - parsed_url = parsed_url._replace(scheme=scheme) - prefer = "uri" # type: str - else: - vcs_type = None - prefer = "file" - - if parsed_url.scheme == "file" and parsed_url.path: - # This is a "file://" URI. Use url_to_path and path_to_url to - # ensure the path is absolute. Also we need to build relpath. - path = Path( - pip_shims.shims.url_to_path(urllib_parse.urlunsplit(parsed_url)) - ).as_posix() - try: - relpath = get_converted_relative_path(path) - except ValueError: - relpath = None - uri = pip_shims.shims.path_to_url(path) - else: - # This is a remote URI. Simply use it. - path = None - relpath = None - # Cut the fragment, but otherwise this is fixed_line. - uri = urllib_parse.urlunsplit( - parsed_url._replace(scheme=original_scheme, fragment="") - ) - - if added_ssh_scheme: - original_uri = urllib_parse.urlunsplit( - original_url._replace(scheme=original_scheme, fragment="") - ) - uri = strip_ssh_from_git_uri(original_uri) - - # Re-attach VCS prefix to build a Link. - link = create_link( - urllib_parse.urlunsplit(parsed_url._replace(scheme=original_scheme)) - ) - - return LinkInfo(vcs_type, prefer, relpath, path, uri, link) - - @property - def setup_info(self): - # type: () -> Optional[SetupInfo] - if self._setup_info is None: - try: - self._setup_info = self.parsed_line.setup_info - except Exception: - self._setup_info = SetupInfo.from_ireq(self.parsed_line.ireq) - if self._setup_info.as_dict().get("requires") is None: - self._setup_info.get_info() - return self._setup_info - - @property - def setup_py_dir(self): - # type: () -> Optional[str] - if self.setup_path: - return os.path.dirname(os.path.abspath(self.setup_path)) - return None - - @property - def dependencies(self): - # type: () -> Tuple[Dict[str, PackagingRequirement], List[Union[str, PackagingRequirement]], List[str]] - build_deps = [] # type: List[Union[str, PackagingRequirement]] - setup_deps = [] # type: List[str] - deps = {} # type: Dict[str, PackagingRequirement] - if self.setup_info: - setup_info = self.setup_info.as_dict() - deps.update(setup_info.get("requires", {})) - setup_deps.extend(setup_info.get("setup_requires", [])) - build_deps.extend(setup_info.get("build_requires", [])) - if self.pyproject_requires: - build_deps.extend(list(self.pyproject_requires)) - setup_deps = list(set(setup_deps)) - build_deps = list(set(build_deps)) - return deps, setup_deps, build_deps - - def __attrs_post_init__(self): - if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: - if self.req is not None: - self._parsed_line._ireq.req = self.req - - @uri.default - def get_uri(self): - # type: () -> str - if self.path and not self.uri: - self._uri_scheme = "path" - return pip_shims.shims.path_to_url(os.path.abspath(self.path)) - elif getattr(self, "req", None) and self.req is not None and getattr(self.req, "url"): - return self.req.url - elif self.link is not None: - return self.link.url_without_fragment - return "" - - @name.default - def get_name(self): - # type: () -> str - loc = self.path or self.uri - if loc and not self._uri_scheme: - self._uri_scheme = "path" if self.path else "file" - name = None - hashed_loc = hashlib.sha256(loc.encode("utf-8")).hexdigest() - hashed_name = hashed_loc[-7:] - if getattr(self, "req", None) and self.req is not None and getattr(self.req, "name") and self.req.name is not None: - if self.is_direct_url and self.req.name != hashed_name: - return self.req.name - if self.link and self.link.egg_fragment and self.link.egg_fragment != hashed_name: - return self.link.egg_fragment - elif self.link and self.link.is_wheel: - from pip_shims import Wheel - self._has_hashed_name = False - return Wheel(self.link.filename).name - elif self.link and ((self.link.scheme == "file" or self.editable) or ( - self.path and self.setup_path and os.path.isfile(str(self.setup_path)) - )): - _ireq = None - if self.editable: - if self.setup_path: - line = pip_shims.shims.path_to_url(self.setup_py_dir) - else: - line = pip_shims.shims.path_to_url(os.path.abspath(self.path)) - if self.extras: - line = "{0}[{1}]".format(line, ",".join(self.extras)) - _ireq = pip_shims.shims.install_req_from_editable(line) - else: - if self.setup_path: - line = Path(self.setup_py_dir).as_posix() - else: - line = Path(os.path.abspath(self.path)).as_posix() - if self.extras: - line = "{0}[{1}]".format(line, ",".join(self.extras)) - _ireq = pip_shims.shims.install_req_from_line(line) - if getattr(self, "req", None) is not None: - _ireq.req = copy.deepcopy(self.req) - if self.extras and _ireq and not _ireq.extras: - _ireq.extras = set(self.extras) - from .setup_info import SetupInfo - subdir = getattr(self, "subdirectory", None) - setupinfo = SetupInfo.from_ireq(self.parsed_line.ireq) - # if self._setup_info is not None: - # setupinfo = self._setup_info - # elif self._parsed_line is not None and self._parsed_line.setup_info is not None: - # setupinfo = self._parsed_line.setup_info - # self._setup_info = self._parsed_line.setup_info - # else: - # setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir) - if setupinfo: - self._setup_info = setupinfo - setupinfo.get_info() - setupinfo_dict = setupinfo.as_dict() - setup_name = setupinfo_dict.get("name", None) - if setup_name: - name = setup_name - self._has_hashed_name = False - build_requires = setupinfo_dict.get("build_requires") - build_backend = setupinfo_dict.get("build_backend") - if build_requires and not self.pyproject_requires: - self.pyproject_requires = tuple(build_requires) - if build_backend and not self.pyproject_backend: - self.pyproject_backend = build_backend - if not name or name.lower() == "unknown": - self._has_hashed_name = True - name = hashed_name - name_in_link = getattr(self.link, "egg_fragment", "") if self.link else "" - if not self._has_hashed_name and name_in_link != name and self.link is not None: - self.link = create_link("{0}#egg={1}".format(self.link.url, name)) - if name is not None: - return name - return "" - - @link.default - def get_link(self): - # type: () -> Link - target = "{0}".format(self.uri) - if hasattr(self, "name") and not self._has_hashed_name: - target = "{0}#egg={1}".format(target, self.name) - link = create_link(target) - return link - - @req.default - def get_requirement(self): - # type: () -> PackagingRequirement - if self.name is None: - if self._parsed_line is not None and self._parsed_line.name is not None: - self.name = self._parsed_line.name - else: - raise ValueError( - "Failed to generate a requirement: missing name for {0!r}".format(self) - ) - req = init_requirement(normalize_name(self.name)) - req.editable = False - if self.link is not None: - req.line = self.link.url_without_fragment - elif self.uri is not None: - req.line = self.uri - else: - req.line = self.name - if self.path and self.link and self.link.scheme.startswith("file"): - req.local_file = True - req.path = self.path - if self.editable: - req.url = None - else: - req.url = self.link.url_without_fragment - else: - req.local_file = False - req.path = None - req.url = self.link.url_without_fragment - if self.editable: - req.editable = True - req.link = self.link - return req - - @property - def parsed_line(self): - # type: () -> Optional[Line] - if self._parsed_line is None: - self._parsed_line = Line(self.line_part) - return self._parsed_line - - @property - def is_local(self): - # type: () -> bool - uri = getattr(self, "uri", None) - if uri is None: - if getattr(self, "path", None) and self.path is not None: - uri = pip_shims.shims.path_to_url(os.path.abspath(self.path)) - elif getattr(self, "req", None) and self.req is not None and ( - getattr(self.req, "url") and self.req.url is not None - ): - uri = self.req.url - if uri and is_file_url(uri): - return True - return False - - @property - def is_remote_artifact(self): - # type: () -> bool - if self.link is None: - return False - return ( - any( - self.link.scheme.startswith(scheme) - for scheme in ("http", "https", "ftp", "ftps", "uri") - ) - and (self.link.is_artifact or self.link.is_wheel) - and not self.editable - ) - - @property - def is_direct_url(self): - # type: () -> bool - if self._parsed_line is not None and self._parsed_line.is_direct_url: - return True - return self.is_remote_artifact - - @property - def formatted_path(self): - # type: () -> Optional[str] - if self.path: - path = self.path - if not isinstance(path, Path): - path = Path(path) - return path.as_posix() - return None - - @classmethod - def create( - cls, - path=None, # type: Optional[str] - uri=None, # type: str - editable=False, # type: bool - extras=None, # type: Optional[Tuple[str]] - link=None, # type: Link - vcs_type=None, # type: Optional[Any] - name=None, # type: Optional[str] - req=None, # type: Optional[Any] - line=None, # type: Optional[str] - uri_scheme=None, # type: str - setup_path=None, # type: Optional[Any] - relpath=None, # type: Optional[Any] - parsed_line=None, # type: Optional[Line] - ): - # type: (...) -> FileRequirement - if parsed_line is None and line is not None: - parsed_line = Line(line) - if relpath and not path: - path = relpath - if not path and uri and link is not None and link.scheme == "file": - path = os.path.abspath(pip_shims.shims.url_to_path(unquote(uri))) - try: - path = get_converted_relative_path(path) - except ValueError: # Vistir raises a ValueError if it can't make a relpath - path = path - if line and not (uri_scheme and uri and link): - vcs_type, uri_scheme, relpath, path, uri, link = cls.get_link_from_line(line) - if not uri_scheme: - uri_scheme = "path" if path else "file" - if path and not uri: - uri = unquote(pip_shims.shims.path_to_url(os.path.abspath(path))) - if not link: - link = cls.get_link_from_line(uri).link - if not uri: - uri = unquote(link.url_without_fragment) - if not extras: - extras = () - pyproject_path = None - pyproject_requires = None - pyproject_backend = None - if path is not None: - pyproject_requires = get_pyproject(path) - if pyproject_requires is not None: - pyproject_requires, pyproject_backend = pyproject_requires - pyproject_requires = tuple(pyproject_requires) - if path: - setup_paths = get_setup_paths(path) - if setup_paths["pyproject_toml"] is not None: - pyproject_path = Path(setup_paths["pyproject_toml"]) - if setup_paths["setup_py"] is not None: - setup_path = Path(setup_paths["setup_py"]).as_posix() - if setup_path and isinstance(setup_path, Path): - setup_path = setup_path.as_posix() - creation_kwargs = { - "editable": editable, - "extras": extras, - "pyproject_path": pyproject_path, - "setup_path": setup_path if setup_path else None, - "uri_scheme": uri_scheme, - "link": link, - "uri": uri, - "pyproject_requires": pyproject_requires, - "pyproject_backend": pyproject_backend, - "path": path or relpath, - "parsed_line": parsed_line - } - if vcs_type: - creation_kwargs["vcs"] = vcs_type - if name: - creation_kwargs["name"] = name - _line = None - ireq = None - setup_info = None - if not name or not parsed_line: - if link is not None and link.url is not None: - _line = unquote(link.url_without_fragment) - if name: - _line = "{0}#egg={1}".format(_line, name) - # if extras: - # _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) - elif uri is not None: - _line = uri - else: - _line = line - if editable: - if extras and ( - (link and link.scheme == "file") or (uri and uri.startswith("file")) - or (not uri and not link) - ): - _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) - if ireq is None: - ireq = pip_shims.shims.install_req_from_editable(_line) - else: - _line = path if (uri_scheme and uri_scheme == "path") else _line - if extras: - _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) - if ireq is None: - ireq = pip_shims.shims.install_req_from_line(_line) - if parsed_line is None: - if editable: - _line = "-e {0}".format(editable) - parsed_line = Line(_line) - if ireq is None: - ireq = parsed_line.ireq - if extras and not ireq.extras: - ireq.extras = set(extras) - if not ireq.is_wheel: - if setup_info is None: - setup_info = SetupInfo.from_ireq(ireq) - setupinfo_dict = setup_info.as_dict() - setup_name = setupinfo_dict.get("name", None) - if setup_name: - name = setup_name - build_requires = setupinfo_dict.get("build_requires", ()) - build_backend = setupinfo_dict.get("build_backend", ()) - if not creation_kwargs.get("pyproject_requires") and build_requires: - creation_kwargs["pyproject_requires"] = tuple(build_requires) - if not creation_kwargs.get("pyproject_backend") and build_backend: - creation_kwargs["pyproject_backend"] = build_backend - creation_kwargs["setup_info"] = setup_info - if path or relpath: - creation_kwargs["path"] = relpath if relpath else path - if req is not None: - creation_kwargs["req"] = req - creation_req = creation_kwargs.get("req") - if creation_kwargs.get("req") is not None: - creation_req_line = getattr(creation_req, "line", None) - if creation_req_line is None and line is not None: - creation_kwargs["req"].line = line # type: ignore - if parsed_line and parsed_line.name: - if name and len(parsed_line.name) != 7 and len(name) == 7: - name = parsed_line.name - if name: - creation_kwargs["name"] = name - cls_inst = cls(**creation_kwargs) # type: ignore - if parsed_line and not cls_inst._parsed_line: - cls_inst._parsed_line = parsed_line - if not cls_inst._parsed_line: - cls_inst._parsed_line = Line(cls_inst.line_part) - if cls_inst._parsed_line and cls_inst.parsed_line.ireq and not cls_inst.parsed_line.ireq.req: - if cls_inst.req: - cls_inst._parsed_line._ireq.req = cls_inst.req - return cls_inst - - @classmethod - def from_line(cls, line, extras=None, parsed_line=None): - # type: (str, Optional[Tuple[str]], Optional[Line]) -> FileRequirement - line = line.strip('"').strip("'") - link = None - path = None - editable = line.startswith("-e ") - line = line.split(" ", 1)[1] if editable else line - setup_path = None - name = None - req = None - if not extras: - extras = () - if not any([is_installable_file(line), is_valid_url(line), is_file_url(line)]): - try: - req = init_requirement(line) - except Exception: - raise RequirementError( - "Supplied requirement is not installable: {0!r}".format(line) - ) - else: - name = getattr(req, "name", None) - line = getattr(req, "url", None) - vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line) - arg_dict = { - "path": relpath if relpath else path, - "uri": unquote(link.url_without_fragment), - "link": link, - "editable": editable, - "setup_path": setup_path, - "uri_scheme": prefer, - "line": line, - "extras": extras, - # "name": name, - } - if req is not None: - arg_dict["req"] = req - if parsed_line is not None: - arg_dict["parsed_line"] = parsed_line - if link and link.is_wheel: - from pip_shims import Wheel - - arg_dict["name"] = Wheel(link.filename).name - elif name: - arg_dict["name"] = name - elif link.egg_fragment: - arg_dict["name"] = link.egg_fragment - return cls.create(**arg_dict) - - @classmethod - def from_pipfile(cls, name, pipfile): - # type: (str, Dict[str, Any]) -> FileRequirement - # Parse the values out. After this dance we should have two variables: - # path - Local filesystem path. - # uri - Absolute URI that is parsable with urlsplit. - # One of these will be a string; the other would be None. - uri = pipfile.get("uri") - fil = pipfile.get("file") - path = pipfile.get("path") - if path: - if isinstance(path, Path) and not path.is_absolute(): - path = get_converted_relative_path(path.as_posix()) - elif not os.path.isabs(path): - path = get_converted_relative_path(path) - if path and uri: - raise ValueError("do not specify both 'path' and 'uri'") - if path and fil: - raise ValueError("do not specify both 'path' and 'file'") - uri = uri or fil - - # Decide that scheme to use. - # 'path' - local filesystem path. - # 'file' - A file:// URI (possibly with VCS prefix). - # 'uri' - Any other URI. - if path: - uri_scheme = "path" - else: - # URI is not currently a valid key in pipfile entries - # see https://github.com/pypa/pipfile/issues/110 - uri_scheme = "file" - - if not uri: - uri = pip_shims.shims.path_to_url(path) - link = cls.get_link_from_line(uri).link - arg_dict = { - "name": name, - "path": path, - "uri": unquote(link.url_without_fragment), - "editable": pipfile.get("editable", False), - "link": link, - "uri_scheme": uri_scheme, - "extras": pipfile.get("extras", None) - } - - extras = pipfile.get("extras", ()) - line = "" - if name: - if extras: - line_name = "{0}[{1}]".format(name, ",".join(sorted(set(extras)))) - else: - line_name = "{0}".format(name) - line = "{0}@ {1}".format(line_name, link.url_without_fragment) - else: - line = link.url - if pipfile.get("editable", False): - line = "-e {0}".format(line) - arg_dict["line"] = line - return cls.create(**arg_dict) - - @property - def line_part(self): - # type: () -> str - link_url = None # type: Optional[str] - seed = None # type: Optional[str] - if self.link is not None: - link_url = unquote(self.link.url_without_fragment) - if self._uri_scheme and self._uri_scheme == "path": - # We may need any one of these for passing to pip - seed = self.path or link_url or self.uri - elif (self._uri_scheme and self._uri_scheme == "file") or ( - (self.link.is_artifact or self.link.is_wheel) and self.link.url - ): - seed = link_url or self.uri - # add egg fragments to remote artifacts (valid urls only) - if not self._has_hashed_name and self.is_remote_artifact and seed is not None: - seed += "#egg={0}".format(self.name) - editable = "-e " if self.editable else "" - if seed is None: - raise ValueError("Could not calculate url for {0!r}".format(self)) - return "{0}{1}".format(editable, seed) - - @property - def pipfile_part(self): - # type: () -> Dict[str, Dict[str, Any]] - excludes = [ - "_base_line", "_has_hashed_name", "setup_path", "pyproject_path", "_uri_scheme", - "pyproject_requires", "pyproject_backend", "setup_info", "_parsed_line" - ] - filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa - pipfile_dict = attr.asdict(self, filter=filter_func).copy() - name = pipfile_dict.pop("name") - if "_uri_scheme" in pipfile_dict: - pipfile_dict.pop("_uri_scheme") - # For local paths and remote installable artifacts (zipfiles, etc) - collision_keys = {"file", "uri", "path"} - collision_order = ["file", "uri", "path"] # type: List[str] - key_match = next(iter(k for k in collision_order if k in pipfile_dict.keys())) - if self._uri_scheme: - dict_key = self._uri_scheme - target_key = ( - dict_key - if dict_key in pipfile_dict - else key_match - ) - if target_key is not None: - winning_value = pipfile_dict.pop(target_key) - collisions = [k for k in collision_keys if k in pipfile_dict] - for key in collisions: - pipfile_dict.pop(key) - pipfile_dict[dict_key] = winning_value - elif ( - self.is_remote_artifact - or (self.link is not None and self.link.is_artifact) - and (self._uri_scheme and self._uri_scheme == "file") - ): - dict_key = "file" - # Look for uri first because file is a uri format and this is designed - # to make sure we add file keys to the pipfile as a replacement of uri - if key_match is not None: - winning_value = pipfile_dict.pop(key_match) - key_to_remove = (k for k in collision_keys if k in pipfile_dict) - for key in key_to_remove: - pipfile_dict.pop(key) - pipfile_dict[dict_key] = winning_value - else: - collisions = [key for key in collision_order if key in pipfile_dict.keys()] - if len(collisions) > 1: - for k in collisions[1:]: - pipfile_dict.pop(k) - return {name: pipfile_dict} - - -@attr.s(slots=True, hash=True) -class VCSRequirement(FileRequirement): - #: Whether the repository is editable - editable = attr.ib(default=None) # type: Optional[bool] - #: URI for the repository - uri = attr.ib(default=None) # type: Optional[str] - #: path to the repository, if it's local - path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) # type: Optional[str] - #: vcs type, i.e. git/hg/svn - vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) # type: Optional[str] - #: vcs reference name (branch / commit / tag) - ref = attr.ib(default=None) # type: Optional[str] - #: Subdirectory to use for installation if applicable - subdirectory = attr.ib(default=None) # type: Optional[str] - _repo = attr.ib(default=None) # type: Optional['VCSRepository'] - _base_line = attr.ib(default=None) # type: Optional[str] - name = attr.ib() - link = attr.ib() - req = attr.ib() - - def __attrs_post_init__(self): - # type: () -> None - if not self.uri: - if self.path: - self.uri = pip_shims.shims.path_to_url(self.path) - split = urllib_parse.urlsplit(self.uri) - scheme, rest = split[0], split[1:] - vcs_type = "" - if "+" in scheme: - vcs_type, scheme = scheme.split("+", 1) - vcs_type = "{0}+".format(vcs_type) - new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) - new_uri = "{0}{1}".format(vcs_type, new_uri) - self.uri = new_uri - if self.req and ( - self.parsed_line.ireq and not self.parsed_line.ireq.req - ): - self.parsed_line._ireq.req = self.req - - @link.default - def get_link(self): - # type: () -> pip_shims.shims.Link - uri = self.uri if self.uri else pip_shims.shims.path_to_url(self.path) - vcs_uri = build_vcs_uri( - self.vcs, - add_ssh_scheme_to_git_uri(uri), - name=self.name, - ref=self.ref, - subdirectory=self.subdirectory, - extras=self.extras, - ) - return self.get_link_from_line(vcs_uri).link - - @name.default - def get_name(self): - # type: () -> Optional[str] - return ( - self.link.egg_fragment or self.req.name - if getattr(self, "req", None) - else super(VCSRequirement, self).get_name() - ) - - @property - def vcs_uri(self): - # type: () -> Optional[str] - uri = self.uri - if not any(uri.startswith("{0}+".format(vcs)) for vcs in VCS_LIST): - uri = "{0}+{1}".format(self.vcs, uri) - return uri - - @req.default - def get_requirement(self): - # type: () -> PkgResourcesRequirement - name = self.name or self.link.egg_fragment - url = None - if self.uri: - url = self.uri - elif self.link is not None: - url = self.link.url_without_fragment - if not name: - raise ValueError( - "pipenv requires an #egg fragment for version controlled " - "dependencies. Please install remote dependency " - "in the form {0}#egg=.".format(url) - ) - req = init_requirement(canonicalize_name(self.name)) - req.editable = self.editable - if not getattr(req, "url"): - if url is not None: - url = add_ssh_scheme_to_git_uri(url) - elif self.uri is not None: - url = self.parse_link_from_line(self.uri).link.url_without_fragment - if url.startswith("git+file:/") and not url.startswith("git+file:///"): - url = url.replace("git+file:/", "git+file:///") - if url: - req.url = url - line = url if url else self.vcs_uri - if self.editable: - line = "-e {0}".format(line) - req.line = line - if self.ref: - req.revision = self.ref - if self.extras: - req.extras = self.extras - req.vcs = self.vcs - if self.path and self.link and self.link.scheme.startswith("file"): - req.local_file = True - req.path = self.path - req.link = self.link - if ( - self.uri != unquote(self.link.url_without_fragment) - and "git+ssh://" in self.link.url - and "git+git@" in self.uri - ): - req.line = self.uri - url = self.link.url_without_fragment - if url.startswith("git+file:/") and not url.startswith("git+file:///"): - url = url.replace("git+file:/", "git+file:///") - req.url = url - return req - - @property - def repo(self): - # type: () -> VCSRepository - if self._repo is None: - self._repo = self.get_vcs_repo() - return self._repo - - def get_checkout_dir(self, src_dir=None): - # type: (Optional[str]) -> str - src_dir = os.environ.get("PIP_SRC", None) if not src_dir else src_dir - checkout_dir = None - if self.is_local: - path = self.path - if not path: - path = pip_shims.shims.url_to_path(self.uri) - if path and os.path.exists(path): - checkout_dir = os.path.abspath(path) - return checkout_dir - if src_dir is not None: - checkout_dir = os.path.join(os.path.abspath(src_dir), self.name) - mkdir_p(src_dir) - return checkout_dir - return os.path.join(create_tracked_tempdir(prefix="requirementslib"), self.name) - - def get_vcs_repo(self, src_dir=None): - # type: (Optional[str]) -> VCSRepository - from .vcs import VCSRepository - - checkout_dir = self.get_checkout_dir(src_dir=src_dir) - vcsrepo = VCSRepository( - url=self.link.url, - name=self.name, - ref=self.ref if self.ref else None, - checkout_directory=checkout_dir, - vcs_type=self.vcs, - subdirectory=self.subdirectory, - ) - if not self.is_local: - vcsrepo.obtain() - pyproject_info = None - if self.subdirectory: - self.setup_path = os.path.join(checkout_dir, self.subdirectory, "setup.py") - self.pyproject_path = os.path.join(checkout_dir, self.subdirectory, "pyproject.toml") - pyproject_info = get_pyproject(os.path.join(checkout_dir, self.subdirectory)) - else: - self.setup_path = os.path.join(checkout_dir, "setup.py") - self.pyproject_path = os.path.join(checkout_dir, "pyproject.toml") - pyproject_info = get_pyproject(checkout_dir) - if pyproject_info is not None: - pyproject_requires, pyproject_backend = pyproject_info - self.pyproject_requires = tuple(pyproject_requires) - self.pyproject_backend = pyproject_backend - return vcsrepo - - def get_commit_hash(self): - # type: () -> str - hash_ = None - hash_ = self.repo.get_commit_hash() - return hash_ - - def update_repo(self, src_dir=None, ref=None): - # type: (Optional[str], Optional[str]) -> str - if ref: - self.ref = ref - else: - if self.ref: - ref = self.ref - repo_hash = None - if not self.is_local and ref is not None: - self.repo.checkout_ref(ref) - repo_hash = self.repo.get_commit_hash() - self.req.revision = repo_hash - return repo_hash - - @contextmanager - def locked_vcs_repo(self, src_dir=None): - # type: (Optional[str]) -> Generator[VCSRepository, None, None] - if not src_dir: - src_dir = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") - vcsrepo = self.get_vcs_repo(src_dir=src_dir) - self.req.revision = vcsrepo.get_commit_hash() - - # Remove potential ref in the end of uri after ref is parsed - if "@" in self.link.show_url and "@" in self.uri: - uri, ref = self.uri.rsplit("@", 1) - checkout = self.req.revision - if checkout and ref in checkout: - self.uri = uri - orig_repo = self._repo - self._repo = vcsrepo - try: - yield vcsrepo - finally: - self._repo = orig_repo - - @classmethod - def from_pipfile(cls, name, pipfile): - # type: (str, Dict[str, Union[List[str], str, bool]]) -> VCSRequirement - creation_args = {} - pipfile_keys = [ - k - for k in ( - "ref", - "vcs", - "subdirectory", - "path", - "editable", - "file", - "uri", - "extras", - ) - + VCS_LIST - if k in pipfile - ] - for key in pipfile_keys: - if key == "extras": - extras = pipfile.get(key, None) - if extras: - pipfile[key] = sorted(dedup([extra.lower() for extra in extras])) - if key in VCS_LIST: - creation_args["vcs"] = key - target = pipfile.get(key) - drive, path = os.path.splitdrive(target) - if ( - not drive - and not os.path.exists(target) - and ( - is_valid_url(target) - or is_file_url(target) - or target.startswith("git@") - ) - ): - creation_args["uri"] = target - else: - creation_args["path"] = target - if os.path.isabs(target): - creation_args["uri"] = pip_shims.shims.path_to_url(target) - else: - creation_args[key] = pipfile.get(key) - creation_args["name"] = name - cls_inst = cls(**creation_args) - if cls_inst._parsed_line is None: - cls_inst._parsed_line = Line(cls_inst.line_part) - if cls_inst.req and ( - cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req - ): - cls_inst._parsed_line.ireq.req = cls_inst.req - return cls_inst - - @classmethod - def from_line(cls, line, editable=None, extras=None, parsed_line=None): - # type: (str, Optional[bool], Optional[Tuple[str]], Optional[Line]) -> VCSRequirement - relpath = None - if parsed_line is None: - parsed_line = Line(line) - if editable: - parsed_line.editable = editable - if extras: - parsed_line.extras = extras - if line.startswith("-e "): - editable = True - line = line.split(" ", 1)[1] - if "@" in line: - parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(line)) - if not parsed.scheme: - possible_name, _, line = line.partition("@") - possible_name = possible_name.strip() - line = line.strip() - possible_name, extras = pip_shims.shims._strip_extras(possible_name) - name = possible_name - line = "{0}#egg={1}".format(line, name) - vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line) - if not extras and link.egg_fragment: - name, extras = pip_shims.shims._strip_extras(link.egg_fragment) - else: - name, _ = pip_shims.shims._strip_extras(link.egg_fragment) - if extras: - extras = parse_extras(extras) - else: - line, extras = pip_shims.shims._strip_extras(line) - if extras: - extras = tuple(extras) - subdirectory = link.subdirectory_fragment - ref = None - if "@" in link.path and "@" in uri: - uri, _, ref = uri.rpartition("@") - if path is not None and "@" in path: - path, _ref = path.rsplit("@", 1) - if ref is None: - ref = _ref - if relpath and "@" in relpath: - relpath, ref = relpath.rsplit("@", 1) - - creation_args = { - "name": name if name else parsed_line.name, - "path": relpath or path, - "editable": editable, - "extras": extras, - "link": link, - "vcs_type": vcs_type, - "line": line, - "uri": uri, - "uri_scheme": prefer, - "parsed_line": parsed_line - } - if relpath: - creation_args["relpath"] = relpath - # return cls.create(**creation_args) - cls_inst = cls( - name=name, - ref=ref, - vcs=vcs_type, - subdirectory=subdirectory, - link=link, - path=relpath or path, - editable=editable, - uri=uri, - extras=extras, - base_line=line, - parsed_line=parsed_line - ) - if cls_inst.req and ( - cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req - ): - cls_inst._parsed_line._ireq.req = cls_inst.req - return cls_inst - - @property - def line_part(self): - # type: () -> str - """requirements.txt compatible line part sans-extras""" - if self.is_local: - base_link = self.link - if not self.link: - base_link = self.get_link() - final_format = ( - "{{0}}#egg={0}".format(base_link.egg_fragment) - if base_link.egg_fragment - else "{0}" - ) - base = final_format.format(self.vcs_uri) - elif self._parsed_line is not None and self._parsed_line.is_direct_url: - return self._parsed_line.line_with_prefix - elif getattr(self, "_base_line", None): - base = self._base_line - else: - base = getattr(self, "link", self.get_link()).url - if base and self.extras and extras_to_string(self.extras) not in base: - if self.subdirectory: - base = "{0}".format(self.get_link().url) - else: - base = "{0}{1}".format(base, extras_to_string(sorted(self.extras))) - if "git+file:/" in base and "git+file:///" not in base: - base = base.replace("git+file:/", "git+file:///") - if self.editable: - base = "-e {0}".format(base) - return base - - @staticmethod - def _choose_vcs_source(pipfile): - # type: (Dict[str, Union[List[str], str, bool]]) -> Dict[str, Union[List[str], str, bool]] - src_keys = [k for k in pipfile.keys() if k in ["path", "uri", "file"]] - if src_keys: - chosen_key = first(src_keys) - vcs_type = pipfile.pop("vcs") - _, pipfile_url = split_vcs_method_from_uri(pipfile.get(chosen_key)) - pipfile[vcs_type] = pipfile_url - for removed in src_keys: - pipfile.pop(removed) - return pipfile - - @property - def pipfile_part(self): - # type: () -> Dict[str, Dict[str, Union[List[str], str, bool]]] - excludes = [ - "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", - "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" - ] - filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa - pipfile_dict = attr.asdict(self, filter=filter_func).copy() - if "vcs" in pipfile_dict: - pipfile_dict = self._choose_vcs_source(pipfile_dict) - name, _ = pip_shims.shims._strip_extras(pipfile_dict.pop("name")) - return {name: pipfile_dict} - - -@attr.s(cmp=True) -class Requirement(object): - name = attr.ib(cmp=True) # type: str - vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[str] - req = attr.ib(default=None, cmp=True) # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] - markers = attr.ib(default=None, cmp=True) # type: Optional[str] - _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[str] - index = attr.ib(default=None) # type: Optional[str] - editable = attr.ib(default=None, cmp=True) # type: Optional[bool] - hashes = attr.ib(default=attr.Factory(tuple), converter=tuple, cmp=True) # type: Optional[Tuple[str]] - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[str]] - abstract_dep = attr.ib(default=None, cmp=False) # type: Optional[AbstractDependency] - _line_instance = attr.ib(default=None, cmp=True) # type: Optional[Line] - _ireq = attr.ib(default=None) # type: Optional[pip_shims.InstallRequirement] - - def __hash__(self): - return hash(self.as_line()) - - @name.default - def get_name(self): - # type: () -> Optional[str] - return self.req.name - - @property - def requirement(self): - # type: () -> Optional[PackagingRequirement] - return self.req.req - - def get_hashes_as_pip(self, as_list=False): - # type: () -> Union[str, List[str]] - if self.hashes: - if as_list: - return [HASH_STRING.format(h) for h in self.hashes] - return "".join([HASH_STRING.format(h) for h in self.hashes]) - return "" if not as_list else [] - - @property - def hashes_as_pip(self): - # type: () -> Union[str, List[str]] - self.get_hashes_as_pip() - - @property - def markers_as_pip(self): - # type: () -> str - if self.markers: - return " ; {0}".format(self.markers).replace('"', "'") - - return "" - - @property - def extras_as_pip(self): - # type: () -> str - if self.extras: - return "[{0}]".format( - ",".join(sorted([extra.lower() for extra in self.extras])) - ) - - return "" - - @property - def commit_hash(self): - # type: () -> Optional[str] - if not self.is_vcs: - return None - commit_hash = None - with self.req.locked_vcs_repo() as repo: - commit_hash = repo.get_commit_hash() - return commit_hash - - @_specifiers.default - def get_specifiers(self): - # type: () -> Optional[str] - if self.req and self.req.req and self.req.req.specifier: - return specs_to_string(self.req.req.specifier) - return "" - - @property - def line_instance(self): - # type: () -> Optional[Line] - include_extras = True - include_specifiers = True - if self.is_vcs: - include_extras = False - if self.is_file_or_url or self.is_vcs or not self._specifiers: - include_specifiers = False - - if self._line_instance is None: - parts = [ - self.req.line_part, - self.extras_as_pip if include_extras else "", - self._specifiers if include_specifiers else "", - self.markers_as_pip, - ] - self._line_instance = Line("".join(parts)) - return self._line_instance - - @property - def specifiers(self): - # type: () -> Optional[str] - if self._specifiers: - return self._specifiers - else: - specs = self.get_specifiers() - if specs: - self._specifiers = specs - return specs - if not self._specifiers and self.req and self.req.req and self.req.req.specifier: - self._specifiers = specs_to_string(self.req.req.specifier) - elif self.is_named and not self._specifiers: - self._specifiers = self.req.version - elif self.req.parsed_line.specifiers and not self._specifiers: - self._specifiers = specs_to_string(self.req.parsed_line.specifiers) - elif self.line_instance.specifiers and not self._specifiers: - self._specifiers = specs_to_string(self.line_instance.specifiers) - elif not self._specifiers and (self.is_file_or_url or self.is_vcs): - try: - setupinfo_dict = self.run_requires() - except Exception: - setupinfo_dict = None - if setupinfo_dict is not None: - self._specifiers = "=={0}".format(setupinfo_dict.get("version")) - if self._specifiers: - specset = SpecifierSet(self._specifiers) - if self.line_instance and not self.line_instance.specifiers: - self.line_instance.specifiers = specset - if self.req and self.req.parsed_line and not self.req.parsed_line.specifiers: - self.req._parsed_line.specifiers = specset - if self.req and self.req.req and not self.req.req.specifier: - self.req.req.specifier = specset - return self._specifiers - - @property - def is_vcs(self): - # type: () -> bool - return isinstance(self.req, VCSRequirement) - - @property - def build_backend(self): - # type: () -> Optional[str] - if self.is_vcs or (self.is_file_or_url and self.req.is_local): - setup_info = self.run_requires() - build_backend = setup_info.get("build_backend") - return build_backend - return "setuptools.build_meta" - - @property - def uses_pep517(self): - # type: () -> bool - if self.build_backend: - return True - return False - - @property - def is_file_or_url(self): - # type: () -> bool - return isinstance(self.req, FileRequirement) - - @property - def is_named(self): - # type: () -> bool - return isinstance(self.req, NamedRequirement) - - @property - def normalized_name(self): - return canonicalize_name(self.name) - - def copy(self): - return attr.evolve(self) - - @classmethod - def from_line(cls, line): - # type: (str) -> Requirement - if isinstance(line, pip_shims.shims.InstallRequirement): - line = format_requirement(line) - hashes = None - if "--hash=" in line: - hashes = line.split(" --hash=") - line, hashes = hashes[0], hashes[1:] - line_instance = Line(line) - editable = line.startswith("-e ") - line = line.split(" ", 1)[1] if editable else line - line, markers = split_markers_from_line(line) - line, extras = pip_shims.shims._strip_extras(line) - if extras: - extras = tuple(parse_extras(extras)) - line = line.strip('"').strip("'").strip() - line_with_prefix = "-e {0}".format(line) if editable else line - vcs = None - # Installable local files and installable non-vcs urls are handled - # as files, generally speaking - line_is_vcs = is_vcs(line) - is_direct_url = False - # check for pep-508 compatible requirements - name, _, possible_url = line.partition("@") - name = name.strip() - if possible_url is not None: - possible_url = possible_url.strip() - is_direct_url = is_valid_url(possible_url) - if not line_is_vcs: - line_is_vcs = is_vcs(possible_url) - r = None # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] - if is_installable_file(line) or ( - (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and - not (line_is_vcs or is_vcs(possible_url)) - ): - r = FileRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) - elif line_is_vcs: - r = VCSRequirement.from_line(line_with_prefix, extras=extras, parsed_line=line_instance) - if isinstance(r, VCSRequirement): - vcs = r.vcs - elif line == "." and not is_installable_file(line): - raise RequirementError( - "Error parsing requirement %s -- are you sure it is installable?" % line - ) - else: - specs = "!=<>~" - spec_matches = set(specs) & set(line) - version = None - name = "{0}".format(line) - if spec_matches: - spec_idx = min((line.index(match) for match in spec_matches)) - name = line[:spec_idx] - version = line[spec_idx:] - if not extras: - name, extras = pip_shims.shims._strip_extras(name) - if extras: - extras = tuple(parse_extras(extras)) - if version: - name = "{0}{1}".format(name, version) - r = NamedRequirement.from_line(line, parsed_line=line_instance) - req_markers = None - if markers: - req_markers = PackagingRequirement("fakepkg; {0}".format(markers)) - if r is not None and r.req is not None: - r.req.marker = getattr(req_markers, "marker", None) if req_markers else None - r.req.local_file = getattr(r.req, "local_file", False) - name = getattr(r, "name", None) - if name is None and getattr(r.req, "name", None) is not None: - name = r.req.name - elif name is None and getattr(r.req, "key", None) is not None: - name = r.req.key - if name is not None and getattr(r.req, "name", None) is None: - r.req.name = name - args = { - "name": name, - "vcs": vcs, - "req": r, - "markers": markers, - "editable": editable, - "line_instance": line_instance - } - if extras: - extras = tuple(sorted(dedup([extra.lower() for extra in extras]))) - args["extras"] = extras - if r is not None: - r.extras = extras - elif r is not None and r.extras is not None: - args["extras"] = tuple(sorted(dedup([extra.lower() for extra in r.extras]))) # type: ignore - if r.req is not None: - r.req.extras = args["extras"] - if hashes: - args["hashes"] = tuple(hashes) # type: ignore - cls_inst = cls(**args) - return cls_inst - - @classmethod - def from_ireq(cls, ireq): - return cls.from_line(format_requirement(ireq)) - - @classmethod - def from_metadata(cls, name, version, extras, markers): - return cls.from_ireq( - make_install_requirement(name, version, extras=extras, markers=markers) - ) - - @classmethod - def from_pipfile(cls, name, pipfile): - from .markers import PipenvMarkers - - _pipfile = {} - if hasattr(pipfile, "keys"): - _pipfile = dict(pipfile).copy() - _pipfile["version"] = get_version(pipfile) - vcs = first([vcs for vcs in VCS_LIST if vcs in _pipfile]) - if vcs: - _pipfile["vcs"] = vcs - r = VCSRequirement.from_pipfile(name, pipfile) - elif any(key in _pipfile for key in ["path", "file", "uri"]): - r = FileRequirement.from_pipfile(name, pipfile) - else: - r = NamedRequirement.from_pipfile(name, pipfile) - markers = PipenvMarkers.from_pipfile(name, _pipfile) - req_markers = None - if markers: - markers = str(markers) - req_markers = PackagingRequirement("fakepkg; {0}".format(markers)) - if r.req is not None: - r.req.marker = req_markers.marker - extras = _pipfile.get("extras") - r.req.specifier = SpecifierSet(_pipfile["version"]) - r.req.extras = ( - tuple(sorted(dedup([extra.lower() for extra in extras]))) if extras else () - ) - args = { - "name": r.name, - "vcs": vcs, - "req": r, - "markers": markers, - "extras": tuple(_pipfile.get("extras", [])), - "editable": _pipfile.get("editable", False), - "index": _pipfile.get("index"), - } - if any(key in _pipfile for key in ["hash", "hashes"]): - args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")]) - cls_inst = cls(**args) - return cls_inst - - def as_line( - self, - sources=None, - include_hashes=True, - include_extras=True, - include_markers=True, - as_list=False, - ): - """Format this requirement as a line in requirements.txt. - - If ``sources`` provided, it should be an sequence of mappings, containing - all possible sources to be used for this requirement. - - If ``sources`` is omitted or falsy, no index information will be included - in the requirement line. - """ - - include_specifiers = True if self.specifiers else False - if self.is_vcs: - include_extras = False - if self.is_file_or_url or self.is_vcs: - include_specifiers = False - parts = [ - self.req.line_part, - self.extras_as_pip if include_extras else "", - self.specifiers if include_specifiers else "", - self.markers_as_pip if include_markers else "", - ] - if as_list: - # This is used for passing to a subprocess call - parts = ["".join(parts)] - if include_hashes: - hashes = self.get_hashes_as_pip(as_list=as_list) - if as_list: - parts.extend(hashes) - else: - parts.append(hashes) - if sources and not (self.requirement.local_file or self.vcs): - from ..utils import prepare_pip_source_args - - if self.index: - sources = [s for s in sources if s.get("name") == self.index] - source_list = prepare_pip_source_args(sources) - if as_list: - parts.extend(sources) - else: - index_string = " ".join(source_list) - parts.extend([" ", index_string]) - if as_list: - return parts - line = "".join(parts) - return line - - def get_markers(self): - # type: () -> Marker - markers = self.markers - if markers: - fake_pkg = PackagingRequirement("fakepkg; {0}".format(markers)) - markers = fake_pkg.markers - return markers - - def get_specifier(self): - # type: () -> Union[SpecifierSet, LegacySpecifier] - try: - return SpecifierSet(self.specifiers) - except InvalidSpecifier: - return LegacySpecifier(self.specifiers) - - def get_version(self): - return pip_shims.shims.parse_version(self.get_specifier().version) - - def get_requirement(self): - req_line = self.req.req.line - if req_line.startswith("-e "): - _, req_line = req_line.split(" ", 1) - req = init_requirement(self.name) - req.line = req_line - req.specifier = SpecifierSet(self.specifiers if self.specifiers else "") - if self.is_vcs or self.is_file_or_url: - req.url = getattr(self.req.req, "url", self.req.link.url_without_fragment) - req.marker = self.get_markers() - req.extras = set(self.extras) if self.extras else set() - return req - - @property - def constraint_line(self): - return self.as_line() - - @property - def is_direct_url(self): - return self.is_file_or_url and self.req.is_direct_url or ( - self.line_instance.is_direct_url or self.req.parsed_line.is_direct_url - ) - - def as_pipfile(self): - good_keys = ( - "hashes", - "extras", - "markers", - "editable", - "version", - "index", - ) + VCS_LIST - req_dict = { - k: v - for k, v in attr.asdict(self, recurse=False, filter=filter_none).items() - if k in good_keys - } - name = self.name - if "markers" in req_dict and req_dict["markers"]: - req_dict["markers"] = req_dict["markers"].replace('"', "'") - base_dict = { - k: v - for k, v in self.req.pipfile_part[name].items() - if k not in ["req", "link", "setup_info"] - } - base_dict.update(req_dict) - conflicting_keys = ("file", "path", "uri") - if "file" in base_dict and any(k in base_dict for k in conflicting_keys[1:]): - conflicts = [k for k in (conflicting_keys[1:],) if k in base_dict] - for k in conflicts: - base_dict.pop(k) - if "hashes" in base_dict: - _hashes = base_dict.pop("hashes") - hashes = [] - for _hash in _hashes: - try: - hashes.append(_hash.as_line()) - except AttributeError: - hashes.append(_hash) - base_dict["hashes"] = sorted(hashes) - if "extras" in base_dict: - base_dict["extras"] = list(base_dict["extras"]) - if len(base_dict.keys()) == 1 and "version" in base_dict: - base_dict = base_dict.get("version") - return {name: base_dict} - - def as_ireq(self): - if self.line_instance and self.line_instance.ireq: - return self.line_instance.ireq - elif getattr(self.req, "_parsed_line", None) and self.req._parsed_line.ireq: - return self.req._parsed_line.ireq - kwargs = { - "include_hashes": False, - } - if (self.is_file_or_url and self.req.is_local) or self.is_vcs: - kwargs["include_markers"] = False - ireq_line = self.as_line(**kwargs) - ireq = Line(ireq_line).ireq - if not getattr(ireq, "req", None): - ireq.req = self.req.req - if (self.is_file_or_url and self.req.is_local) or self.is_vcs: - if getattr(ireq, "req", None) and getattr(ireq.req, "marker", None): - ireq.req.marker = None - else: - ireq.req.extras = self.req.req.extras - if not ((self.is_file_or_url and self.req.is_local) or self.is_vcs): - ireq.req.marker = self.req.req.marker - return ireq - - @property - def pipfile_entry(self): - return self.as_pipfile().copy().popitem() - - @property - def ireq(self): - return self.as_ireq() - - def get_dependencies(self, sources=None): - """Retrieve the dependencies of the current requirement. - - Retrieves dependencies of the current requirement. This only works on pinned - requirements. - - :param sources: Pipfile-formatted sources, defaults to None - :param sources: list[dict], optional - :return: A set of requirement strings of the dependencies of this requirement. - :rtype: set(str) - """ - - from .dependencies import get_dependencies - - if not sources: - sources = [ - {"name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": True} - ] - return get_dependencies(self.as_ireq(), sources=sources) - - def get_abstract_dependencies(self, sources=None): - """Retrieve the abstract dependencies of this requirement. - - Returns the abstract dependencies of the current requirement in order to resolve. - - :param sources: A list of sources (pipfile format), defaults to None - :param sources: list, optional - :return: A list of abstract (unpinned) dependencies - :rtype: list[ :class:`~requirementslib.models.dependency.AbstractDependency` ] - """ - - from .dependencies import ( - AbstractDependency, - get_dependencies, - get_abstract_dependencies, - ) - - if not self.abstract_dep: - parent = getattr(self, "parent", None) - self.abstract_dep = AbstractDependency.from_requirement(self, parent=parent) - if not sources: - sources = [ - {"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True} - ] - if is_pinned_requirement(self.ireq): - deps = self.get_dependencies() - else: - ireq = sorted(self.find_all_matches(), key=lambda k: k.version) - deps = get_dependencies(ireq.pop(), sources=sources) - return get_abstract_dependencies( - deps, sources=sources, parent=self.abstract_dep - ) - - def find_all_matches(self, sources=None, finder=None): - """Find all matching candidates for the current requirement. - - Consults a finder to find all matching candidates. - - :param sources: Pipfile-formatted sources, defaults to None - :param sources: list[dict], optional - :return: A list of Installation Candidates - :rtype: list[ :class:`~pip._internal.index.InstallationCandidate` ] - """ - - from .dependencies import get_finder, find_all_matches - - if not finder: - finder = get_finder(sources=sources) - return find_all_matches(finder, self.as_ireq()) - - def run_requires(self, sources=None, finder=None): - if self.req and self.req.setup_info is not None: - info_dict = self.req.setup_info.as_dict() - elif self.line_instance and self.line_instance.setup_info is not None: - info_dict = self.line_instance.setup_info.as_dict() - else: - from .setup_info import SetupInfo - if not finder: - from .dependencies import get_finder - finder = get_finder(sources=sources) - info = SetupInfo.from_requirement(self, finder=finder) - if info is None: - return {} - info_dict = info.get_info() - if self.req and not self.req.setup_info: - self.req._setup_info = info - if self.req._has_hashed_name and info_dict.get("name"): - self.req.name = self.name = info_dict["name"] - if self.req.req.name != info_dict["name"]: - self.req.req.name = info_dict["name"] - return info_dict - - def merge_markers(self, markers): - if not isinstance(markers, Marker): - markers = Marker(markers) - _markers = set(Marker(self.ireq.markers)) if self.ireq.markers else set(markers) - _markers.add(markers) - new_markers = Marker(" or ".join([str(m) for m in sorted(_markers)])) - self.markers = str(new_markers) - self.req.req.marker = new_markers diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 90472d12..921c14cd 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -1906,10 +1906,10 @@ def __attrs_post_init__(self): new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) new_uri = "{0}{1}".format(vcs_type, new_uri) self.uri = new_uri - if self.req and ( - self.parsed_line.ireq and not self.parsed_line.ireq.req + if self.req and self._parsed_line and ( + self._parsed_line.ireq and not self._parsed_line.ireq.req ): - self.parsed_line._ireq.req = self.req + self._parsed_line._ireq.req = self.req @link.default def get_link(self): @@ -2461,6 +2461,13 @@ def line_instance(self): self._line_instance = Line(line) return self._line_instance + @line_instance.setter + def line_instance(self, line_instance): + # type: (Line) -> None + if self.req and not self.req._parsed_line: + self.req._parsed_line = line_instance + self._line_instance = line_instance + @property def specifiers(self): # type: () -> Optional[Text] @@ -2560,73 +2567,11 @@ def from_line(cls, line): ) else: r = named_req_from_parsed_line(parsed_line) - # hashes = None - # if "--hash=" in line: - # hashes = line.split(" --hash=") - # line, hashes = hashes[0], hashes[1:] - # editable = line.startswith("-e ") - # line = line.split(" ", 1)[1] if editable else line - # line, markers = split_markers_from_line(line) - # line, extras = pip_shims.shims._strip_extras(line) - # if extras: - # extras = tuple(parse_extras(extras)) - # line = line.strip('"').strip("'").strip() - # line_with_prefix = "-e {0}".format(line) if editable else line - # vcs = None - # # Installable local files and installable non-vcs urls are handled - # # as files, generally speaking - # line_is_vcs = is_vcs(line) - # is_direct_url = False - # # check for pep-508 compatible requirements - # name, _, possible_url = line.partition("@") - # name = name.strip() - # if possible_url is not None: - # possible_url = possible_url.strip() - # is_direct_url = is_valid_url(possible_url) - # if not line_is_vcs: - # line_is_vcs = is_vcs(possible_url) - # if is_installable_file(line) or ( - # (is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and - # not (line_is_vcs or is_vcs(possible_url)) - # ): - # r = FileRequirement.from_line(line_with_prefix, extras=extras, parsed_line=parsed_line) - # elif line_is_vcs: - # r = VCSRequirement.from_line(line_with_prefix, extras=extras, parsed_line=parsed_line) - # if isinstance(r, VCSRequirement): - # vcs = r.vcs - # elif line == "." and not is_installable_file(line): - # raise RequirementError( - # "Error parsing requirement %s -- are you sure it is installable?" % line - # ) - # else: - # specs = "!=<>~" - # spec_matches = set(specs) & set(line) - # version = None - # name = "{0}".format(line) - # if spec_matches: - # spec_idx = min((line.index(match) for match in spec_matches)) - # name = line[:spec_idx] - # version = line[spec_idx:] - # if not extras: - # name, extras = pip_shims.shims._strip_extras(name) - # if extras: - # extras = tuple(parse_extras(extras)) - # if version: - # name = "{0}{1}".format(name, version) - # r = NamedRequirement.from_line(line, parsed_line=parsed_line) req_markers = None if parsed_line.markers: req_markers = PackagingRequirement("fakepkg; {0}".format(parsed_line.markers)) if r is not None and r.req is not None: r.req.marker = getattr(req_markers, "marker", None) if req_markers else None - # r.req.local_file = getattr(r.req, "local_file", False) - # name = getattr(r, "name", None) - # if name is None and getattr(r.req, "name", None) is not None: - # name = r.req.name - # elif name is None and getattr(r.req, "key", None) is not None: - # name = r.req.key - # if name is not None and getattr(r.req, "name", None) is None: - # r.req.name = name args = { "name": r.name, "vcs": parsed_line.vcs, @@ -2701,17 +2646,7 @@ def from_pipfile(cls, name, pipfile): if any(key in _pipfile for key in ["hash", "hashes"]): args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")]) cls_inst = cls(**args) - # if not cls_inst.req._parsed_line: - # parsed_line = Line(cls_inst.as_line()) - # cls_inst.req._parsed_line = parsed_line - # if not cls_inst.line_instance: - # cls_inst.line_instance = parsed_line - # if not cls_inst.is_named and not cls_inst.req._setup_info and parsed_line.setup_info: - # cls_inst.req._setup_info = parsed_line.setup_info - # if not cls_inst.req.name and parsed_line.setup_info.name: - # cls_inst.name = cls_inst.req.name = parsed_line.setup_info.name - # if not cls_inst.req.name and parsed_line.name: - # cls_inst.name = cls_inst.req.name = parsed_line.name + cls_inst.line_instance = Line(cls_inst.as_line()) return cls_inst def as_line( diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 839e229e..59b0eacd 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -21,7 +21,7 @@ from six.moves.urllib.parse import unquote, urlparse, urlunparse from vistir.compat import Iterable, Path -from vistir.contextmanagers import cd, temp_path, replaced_streams +from vistir.contextmanagers import cd, temp_path from vistir.misc import run from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p, rmtree @@ -69,7 +69,7 @@ @contextlib.contextmanager def _suppress_distutils_logs(): - # type: () -> None + # type: () -> Generator[None, None, None] """Hack to hide noise generated by `setup.py develop`. There isn't a good way to suppress them now, so let's monky-patch. @@ -89,7 +89,7 @@ def _log(log, level, msg, args): @ensure_mkdir_p(mode=0o775) def _get_src_dir(root): - # type: (str) -> str + # type: (Text) -> Text src = os.environ.get("PIP_SRC") if src: return src @@ -105,7 +105,7 @@ def _get_src_dir(root): def ensure_reqs(reqs): - # type: (List[Union[str, PkgResourcesRequirement]]) -> List[PkgResourcesRequirement] + # type: (List[Union[Text, PkgResourcesRequirement]]) -> List[PkgResourcesRequirement] import pkg_resources if not isinstance(reqs, Iterable): raise TypeError("Expecting an Iterable, got %r" % reqs) @@ -121,7 +121,7 @@ def ensure_reqs(reqs): def pep517_subprocess_runner(cmd, cwd=None, extra_environ=None): - # type: (List[str], Optional[str], Optional[Dict[str, str]]) -> None + # type: (List[Text], Optional[Text], Optional[Dict[Text, Text]]) -> None """The default method of calling the wrapper subprocess.""" env = os.environ.copy() if extra_environ: @@ -132,18 +132,18 @@ def pep517_subprocess_runner(cmd, cwd=None, extra_environ=None): def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, editable=False): - # type: (Optional[InstallRequirement], Optional[str], Optional[str], bool) -> Dict[str, str] - download_dir = os.path.join(CACHE_DIR, "pkgs") # type: str + # type: (Optional[InstallRequirement], Optional[Text], Optional[Text], bool) -> Dict[Text, Text] + download_dir = os.path.join(CACHE_DIR, "pkgs") # type: Text mkdir_p(download_dir) - wheel_download_dir = os.path.join(CACHE_DIR, "wheels") # type: str + wheel_download_dir = os.path.join(CACHE_DIR, "wheels") # type: Text mkdir_p(wheel_download_dir) if src_dir is None: if editable and src_root is not None: src_dir = src_root elif ireq is None and src_root is not None: - src_dir = _get_src_dir(root=src_root) # type: str + src_dir = _get_src_dir(root=src_root) # type: Text elif ireq is not None and ireq.editable and src_root is not None: src_dir = _get_src_dir(root=src_root) else: @@ -163,7 +163,7 @@ def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, edita def iter_metadata(path, pkg_name=None, metadata_type="egg-info"): - # type: (str, Optional[str], str) -> Generator + # type: (Text, Optional[Text], Text) -> Generator if pkg_name is not None: pkg_variants = get_name_variants(pkg_name) non_matching_dirs = [] @@ -181,7 +181,7 @@ def iter_metadata(path, pkg_name=None, metadata_type="egg-info"): def find_egginfo(target, pkg_name=None): - # type: (str, Optional[str]) -> Generator + # type: (Text, Optional[Text]) -> Generator egg_dirs = ( egg_dir for egg_dir in iter_metadata(target, pkg_name=pkg_name) if egg_dir is not None @@ -194,7 +194,7 @@ def find_egginfo(target, pkg_name=None): def find_distinfo(target, pkg_name=None): - # type: (str, Optional[str]) -> Generator + # type: (Text, Optional[Text]) -> Generator dist_dirs = ( dist_dir for dist_dir in iter_metadata(target, pkg_name=pkg_name, metadata_type="dist-info") if dist_dir is not None @@ -207,7 +207,7 @@ def find_distinfo(target, pkg_name=None): def get_metadata(path, pkg_name=None, metadata_type=None): - # type: (str, Optional[str], Optional[str]) -> Dict[str, Union[str, List[RequirementType], Dict[str, RequirementType]]] + # type: (Text, Optional[Text], Optional[Text]) -> Dict[Text, Union[Text, List[RequirementType], Dict[Text, RequirementType]]] metadata_dirs = [] wheel_allowed = metadata_type == "wheel" or metadata_type is None egg_allowed = metadata_type == "egg" or metadata_type is None @@ -329,24 +329,24 @@ def get_metadata_from_dist(dist): @attr.s(slots=True, frozen=True) class BaseRequirement(object): - name = attr.ib(type=str, default="", cmp=True) + name = attr.ib(default="", cmp=True) # type: Text requirement = attr.ib(default=None, cmp=True) # type: Optional[PkgResourcesRequirement] def __str__(self): - # type: () -> str + # type: () -> Text return "{0}".format(str(self.requirement)) def as_dict(self): - # type: () -> Dict[str, Optional[PkgResourcesRequirement]] + # type: () -> Dict[Text, Optional[PkgResourcesRequirement]] return {self.name: self.requirement} def as_tuple(self): - # type: () -> Tuple[str, Optional[PkgResourcesRequirement]] + # type: () -> Tuple[Text, Optional[PkgResourcesRequirement]] return (self.name, self.requirement) @classmethod def from_string(cls, line): - # type: (str) -> BaseRequirement + # type: (Text) -> BaseRequirement line = line.strip() req = init_requirement(line) return cls.from_req(req) @@ -367,11 +367,11 @@ def from_req(cls, req): @attr.s(slots=True, frozen=True) class Extra(object): - name = attr.ib(type=str, default=None, cmp=True) + name = attr.ib(default=None, cmp=True) # type: Text requirements = attr.ib(factory=frozenset, cmp=True, type=frozenset) def __str__(self): - # type: () -> str + # type: () -> Text return "{0}: {{{1}}}".format(self.section, ", ".join([r.name for r in self.requirements])) def add(self, req): @@ -381,18 +381,18 @@ def add(self, req): return self def as_dict(self): - # type: () -> Dict[str, Tuple[PkgResourcesRequirement]] + # type: () -> Dict[Text, Tuple[RequirementType, ...]] return {self.name: tuple([r.requirement for r in self.requirements])} @attr.s(slots=True, cmp=True, hash=True) class SetupInfo(object): - name = attr.ib(type=str, default=None, cmp=True) - base_dir = attr.ib(type=str, default=None, cmp=True, hash=False) - version = attr.ib(type=str, default=None, cmp=True) + name = attr.ib(default=None, cmp=True) # type: Text + base_dir = attr.ib(default=None, cmp=True, hash=False) # type: Text + version = attr.ib(default=None, cmp=True) # type: Text _requirements = attr.ib(type=frozenset, factory=frozenset, cmp=True, hash=True) build_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) - build_backend = attr.ib(type=str, default="setuptools.build_meta:__legacy__", cmp=True) + build_backend = attr.ib(default="setuptools.build_meta:__legacy__", cmp=True) # type: Text setup_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None, cmp=True) _extras_requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) @@ -401,16 +401,16 @@ class SetupInfo(object): pyproject = attr.ib(type=Path, default=None, cmp=True, hash=False) ireq = attr.ib(default=None, cmp=True, hash=False) # type: Optional[InstallRequirement] extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict, cmp=False, hash=False) - metadata = attr.ib(default=None) # type: Optional[Tuple[str]] + metadata = attr.ib(default=None) # type: Optional[Tuple[Text]] @property def requires(self): - # type: () -> Dict[str, RequirementType] + # type: () -> Dict[Text, RequirementType] return {req.name: req.requirement for req in self._requirements} @property def extras(self): - # type: () -> Dict[str, Dict[str, List[RequirementType]]] + # type: () -> Dict[Text, Optional[Any]] extras_dict = {} extras = set(self._extras_requirements) for section, deps in extras: @@ -422,6 +422,7 @@ def extras(self): @classmethod def get_setup_cfg(cls, setup_cfg_path): + # type: (Text) -> Dict[Text, Union[Text, None, Set[BaseRequirement], List[Text], Tuple[Text, Tuple[BaseRequirement]]]] if os.path.exists(setup_cfg_path): default_opts = { "metadata": {"name": "", "version": ""}, @@ -440,7 +441,7 @@ def get_setup_cfg(cls, setup_cfg_path): results["name"] = parser.get("metadata", "name") if parser.has_option("metadata", "version"): results["version"] = parser.get("metadata", "version") - install_requires = set() + install_requires = set() # type: Set(BaseRequirement) if parser.has_option("options", "install_requires"): install_requires = set([ BaseRequirement.from_string(dep) @@ -470,7 +471,7 @@ def get_setup_cfg(cls, setup_cfg_path): @property def egg_base(self): - base = None # type: Optional[Path] + base = None # type: Optional[Text] if self.setup_py.exists(): base = self.setup_py.parent elif self.pyproject.exists(): @@ -499,7 +500,7 @@ def parse_setup_cfg(self): if self.build_requires: self.build_requires = tuple(set(self.build_requires) | set(build_requires)) self._requirements = frozenset( - set(self._requirements) | parsed["install_requires"] + set(self._requirements) | set(parsed["install_requires"]) ) if self.python_requires is None: self.python_requires = parsed.get("python_requires") @@ -639,7 +640,7 @@ def build_pep517(self, hookcaller): return dist_path def reload(self): - # type: () -> Dict[str, Any] + # type: () -> Dict[Text, Any] """ Wipe existing distribution info metadata for rebuilding. """ @@ -657,7 +658,7 @@ def get_metadata_from_wheel(self, wheel_path): self.populate_metadata(metadata_dict) def get_egg_metadata(self, metadata_dir=None, metadata_type=None): - # type: (Optional[str], Optional[str]) -> None + # type: (Optional[Text], Optional[Text]) -> None package_indicators = [self.pyproject, self.setup_py, self.setup_cfg] # if self.setup_py is not None and self.setup_py.exists(): metadata_dirs = [] @@ -715,12 +716,12 @@ def run_pyproject(self): self.build_requires = tuple(requires) def get_info(self): - # type: () -> Dict[str, Any] + # type: () -> Dict[Text, Any] if self.setup_cfg and self.setup_cfg.exists(): with cd(self.base_dir): self.parse_setup_cfg() - with cd(self.base_dir), replaced_streams(): + with cd(self.base_dir): self.run_pyproject() self.build() @@ -739,7 +740,7 @@ def get_info(self): return self.as_dict() def as_dict(self): - # type: () -> Dict[str, Any] + # type: () -> Dict[Text, Any] prop_dict = { "name": self.name, "version": self.version, @@ -760,13 +761,14 @@ def as_dict(self): @classmethod def from_requirement(cls, requirement, finder=None): - # type: (TRequirement, PackageFinder) + # type: (TRequirement, Optional[PackageFinder]) -> Optional[SetupInfo] ireq = requirement.as_ireq() subdir = getattr(requirement.req, "subdirectory", None) return cls.from_ireq(ireq, subdir=subdir, finder=finder) @classmethod def from_ireq(cls, ireq, subdir=None, finder=None): + # type: (InstallRequirement, Optional[Text], Optional[PackageFinder]) -> Optional[SetupInfo] import pip_shims.shims if not ireq.link: return @@ -835,6 +837,7 @@ def from_ireq(cls, ireq, subdir=None, finder=None): @classmethod def create(cls, base_dir, subdirectory=None, ireq=None, kwargs=None): + # type: (Text, Optional[Text], Optional[InstallRequirement], Optional[Dict[Text, Text]]) -> Optional[SetupInfo] if not base_dir or base_dir is None: return diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index 10eda5a2..f49e9e7c 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -33,11 +33,18 @@ from attr import _ValidatorType from packaging.requirements import Requirement as PackagingRequirement from pkg_resources import Requirement as PkgResourcesRequirement - from pkg_resources.extern.packaging.markers import Marker as PkgResourcesMarker + from pkg_resources.extern.packaging.markers import ( + Op as PkgResourcesOp, Variable as PkgResourcesVariable, + Value as PkgResourcesValue, Marker as PkgResourcesMarker + ) from pip_shims.shims import Link from vistir.compat import Path _T = TypeVar("_T") TMarker = Union[Marker, PkgResourcesMarker] + TVariable = TypeVar("TVariable", PkgResourcesVariable, Variable) + TValue = TypeVar("TValue", PkgResourcesValue, Value) + TOp = TypeVar("TOp", PkgResourcesOp, Op) + MarkerTuple = Tuple[TVariable, TOp, TValue] TRequirement = Union[PackagingRequirement, PkgResourcesRequirement] @@ -284,7 +291,8 @@ def strip_extras_markers_from_requirement(req): """ if req is None: raise TypeError("Must pass in a valid requirement, received {0!r}".format(req)) - if req.marker is not None: + if getattr(req, "marker", None) is not None: + marker = req.marker # type: TMarker req.marker._markers = _strip_extras_markers(req.marker._markers) if not req.marker._markers: req.marker = None @@ -292,7 +300,7 @@ def strip_extras_markers_from_requirement(req): def _strip_extras_markers(marker): - # type: (TMarker) -> TMarker + # type: (Union[MarkerTuple, List[Union[MarkerTuple, str]]]) -> List[Union[MarkerTuple, str]] if marker is None or not isinstance(marker, (list, tuple)): raise TypeError("Expecting a marker type, received {0!r}".format(marker)) markers_to_remove = [] diff --git a/tasks/__init__.py b/tasks/__init__.py index 9d482745..0639287e 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -6,11 +6,13 @@ from . import vendoring from . import news +import enum import pathlib import shutil import subprocess import parver import re +import sys from pathlib import Path from towncrier._builder import ( @@ -25,6 +27,14 @@ INIT_PY = ROOT.joinpath('src', PACKAGE_NAME, '__init__.py') +class LogLevel(enum.Enum): + WARN = 30 + ERROR = 40 + DEBUG = 10 + INFO = 20 + CRITICAL = 50 + + def _get_git_root(ctx): return Path(ctx.run('git rev-parse --show-toplevel', hide=True).stdout.strip()) @@ -37,6 +47,14 @@ def find_version(): raise RuntimeError("Unable to find version string.") +@invoke.task() +def typecheck(ctx): + src_dir = ROOT / "src" / PACKAGE_NAME + src_dir = src_dir.as_posix() + env = {"MYPYPATH": src_dir} + ctx.run(f"mypy {src_dir}", env=env) + + @invoke.task() def clean(ctx): """Clean previously built package artifacts. @@ -180,4 +198,38 @@ def clean_mdchangelog(ctx): changelog.write_text(content) -ns = invoke.Collection(build_docs, vendoring, news, release, clean_mdchangelog) +def log(task, message, level=LogLevel.INFO): + message_format = f"[{level.name.upper()}] " + if level >= LogLevel.ERROR: + task = f"****** ({task}) " + else: + task = f"({task}) " + print(f"{message_format}{task}{message}", file=sys.stderr) + + +@invoke.task +def profile(ctx, filepath, calltree=False): + """ Run and profile a given Python script. + + :param str filepath: The filepath of the script to profile + """ + + filepath = pathlib.Path(filepath) + if not filepath.is_file(): + log("profile", f"no such script {filepath!s}", LogLevel.ERROR) + else: + if calltree: + log("profile", f"profiling script {filepath!s} calltree") + ctx.run( + ( + f"python -m cProfile -o .profile.cprof {filepath!s}" + " && pyprof2calltree -k -i .profile.cprof" + " && rm -rf .profile.cprof" + ) + ) + else: + log("profile", f"profiling script {filepath!s}") + ctx.run(f"vprof -c cmhp {filepath!s}") + + +ns = invoke.Collection(build_docs, vendoring, news, release, clean_mdchangelog, profile, typecheck) diff --git a/tests/unit/test_setup_info.py b/tests/unit/test_setup_info.py index bf061b89..0e6f6cce 100644 --- a/tests/unit/test_setup_info.py +++ b/tests/unit/test_setup_info.py @@ -42,8 +42,19 @@ def test_no_duplicate_egg_info(): base_dir = vistir.compat.Path(os.path.abspath(os.getcwd())).as_posix() r = Requirement.from_line("-e {}".format(base_dir)) egg_info_name = "{}.egg-info".format(r.name.replace("-", "_")) - assert os.path.isdir(os.path.join(base_dir, "reqlib-metadata", egg_info_name)) - assert not os.path.isdir(os.path.join(base_dir, egg_info_name)) + distinfo_name = "{0}.dist-info".format(r.name.replace("-", "_")) + + def find_metadata(path): + metadata_names = [ + os.path.join(path, name) for name in (egg_info_name, distinfo_name) + ] + return next(iter(pth for pth in metadata_names if os.path.isdir(pth)), None) + + assert not find_metadata(base_dir) + assert not find_metadata(os.path.join(base_dir, "reqlib-metadata")) + assert not find_metadata(os.path.join(base_dir, "src", "reqlib-metadata")) + assert r.req.setup_info and os.path.isdir(r.req.setup_info.egg_base) + assert find_metadata(r.req.setup_info.egg_base) def test_without_extras(pathlib_tmpdir): @@ -122,10 +133,4 @@ def test_extras(pathlib_tmpdir): assert r.name == "test-package" r.req.setup_info.get_info() setup_dict = r.req.setup_info.as_dict() - import sys - for k, v in setup_dict.items(): - print("{0}: {1}".format(k, v), file=sys.stderr) - if k in ("base_dir",): - print(" dir contents: %s" % os.listdir(v)) - # assert setup_dict == "", setup_dict assert sorted(list(setup_dict.get("requires").keys())) == ["coverage", "flaky", "six"], setup_dict From e912e2189a2fe838c65963ed00406d5a6cd4c6a1 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Thu, 14 Feb 2019 20:35:54 -0500 Subject: [PATCH 15/35] Change parse order to prioritize line instances - Fix setup info generation for VCS requirements - Use PEP517 builder - Add minor patches for pep517 builder Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 89 +++++++++++----------- src/requirementslib/models/setup_info.py | 70 +++++++++-------- src/requirementslib/models/utils.py | 16 ++-- tasks/__init__.py | 2 +- 4 files changed, 96 insertions(+), 81 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 921c14cd..b25a6e1c 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -1666,13 +1666,13 @@ def create( if name: creation_kwargs["name"] = name cls_inst = cls(**creation_kwargs) # type: ignore - if parsed_line and not cls_inst._parsed_line: - cls_inst._parsed_line = parsed_line - if not cls_inst._parsed_line: - cls_inst._parsed_line = Line(cls_inst.line_part) - if cls_inst._parsed_line and cls_inst.parsed_line.ireq and not cls_inst.parsed_line.ireq.req: - if cls_inst.req: - cls_inst._parsed_line._ireq.req = cls_inst.req + # if parsed_line and not cls_inst._parsed_line: + # cls_inst._parsed_line = parsed_line + # if not cls_inst._parsed_line: + # cls_inst._parsed_line = Line(cls_inst.line_part) + # if cls_inst._parsed_line and cls_inst.parsed_line.ireq and not cls_inst.parsed_line.ireq.req: + # if cls_inst.req: + # cls_inst._parsed_line._ireq.req = cls_inst.req return cls_inst @classmethod @@ -1906,10 +1906,10 @@ def __attrs_post_init__(self): new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) new_uri = "{0}{1}".format(vcs_type, new_uri) self.uri = new_uri - if self.req and self._parsed_line and ( - self._parsed_line.ireq and not self._parsed_line.ireq.req - ): - self._parsed_line._ireq.req = self.req + # if self.req and self._parsed_line and ( + # self._parsed_line.ireq and not self._parsed_line.ireq.req + # ): + # self._parsed_line._ireq.req = self.req @link.default def get_link(self): @@ -1944,15 +1944,15 @@ def vcs_uri(self): @property def setup_info(self): + if self._parsed_line and self._parsed_line.setup_info: + if not self._parsed_line.setup_info.name: + self._parsed_line._setup_info.get_info() + return self._parsed_line.setup_info if self._repo: from .setup_info import SetupInfo self._setup_info = SetupInfo.from_ireq(Line(self._repo.checkout_directory).ireq) self._setup_info.get_info() return self._setup_info - if self._parsed_line and self._parsed_line.setup_info: - if not self._parsed_line.setup_info.name: - self._parsed_line._setup_info.get_info() - return self._parsed_line.setup_info ireq = self.parsed_line.ireq from .setup_info import SetupInfo self._setup_info = SetupInfo.from_ireq(ireq) @@ -2019,9 +2019,12 @@ def get_requirement(self): def repo(self): # type: () -> VCSRepository if self._repo is None: - self._repo = self.get_vcs_repo() - if self._parsed_line: - self._parsed_line.vcsrepo = self._repo + if self._parsed_line and self._parsed_line.vcsrepo: + self._repo = self._parsed_line.vcsrepo + else: + self._repo = self.get_vcs_repo() + if self._parsed_line: + self._parsed_line.vcsrepo = self._repo return self._repo def get_checkout_dir(self, src_dir=None): @@ -2041,11 +2044,12 @@ def get_checkout_dir(self, src_dir=None): return checkout_dir return os.path.join(create_tracked_tempdir(prefix="requirementslib"), self.name) - def get_vcs_repo(self, src_dir=None): - # type: (Optional[Text]) -> VCSRepository + def get_vcs_repo(self, src_dir=None, checkout_dir=None): + # type: (Optional[Text], Optional[Text]) -> VCSRepository from .vcs import VCSRepository - checkout_dir = self.get_checkout_dir(src_dir=src_dir) + if checkout_dir is None: + checkout_dir = self.get_checkout_dir(src_dir=src_dir) vcsrepo = VCSRepository( url=self.link.url, name=self.name, @@ -2115,12 +2119,11 @@ def locked_vcs_repo(self, src_dir=None): if self._parsed_line: self._parsed_line.vcsrepo = vcsrepo if self._setup_info: - self._setup_info._requirements = () - self._setup_info._extras_requirements = () - self._setup_info.build_requires = () - self._setup_info.setup_requires = () - self._setup_info.version = None - self._setup_info.metadata = None + _old_setup_info = self._setup_info + self._setup_info = attr.evolve( + self._setup_info, requirements=(), _extras_requirements=(), + build_requires=(), setup_requires=(), version=None, metadata=None + ) if self.parsed_line: self._parsed_line.vcsrepo = vcsrepo # self._parsed_line._specifier = "=={0}".format(self.setup_info.version) @@ -2131,6 +2134,7 @@ def locked_vcs_repo(self, src_dir=None): yield vcsrepo finally: self._repo = orig_repo + # self._setup_info = _old_setup_info @classmethod def from_pipfile(cls, name, pipfile): @@ -2178,21 +2182,21 @@ def from_pipfile(cls, name, pipfile): creation_args[key] = pipfile.get(key) creation_args["name"] = name cls_inst = cls(**creation_args) - if cls_inst._parsed_line is None: - vcs_uri = build_vcs_uri( - vcs=cls_inst.vcs, uri=add_ssh_scheme_to_git_uri(cls_inst.uri), - name=cls_inst.name, ref=cls_inst.ref, subdirectory=cls_inst.subdirectory, - extras=cls_inst.extras - ) - if cls_inst.editable: - vcs_uri = "-e {0}".format(vcs_uri) - cls_inst._parsed_line = Line(vcs_uri) - if not cls_inst.name and cls_inst._parsed_line.name: - cls_inst.name = cls_inst._parsed_line.name - if cls_inst.req and ( - cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req - ): - cls_inst._parsed_line.ireq.req = cls_inst.req + # if cls_inst._parsed_line is None: + # vcs_uri = build_vcs_uri( + # vcs=cls_inst.vcs, uri=add_ssh_scheme_to_git_uri(cls_inst.uri), + # name=cls_inst.name, ref=cls_inst.ref, subdirectory=cls_inst.subdirectory, + # extras=cls_inst.extras + # ) + # if cls_inst.editable: + # vcs_uri = "-e {0}".format(vcs_uri) + # cls_inst._parsed_line = Line(vcs_uri) + # if not cls_inst.name and cls_inst._parsed_line.name: + # cls_inst.name = cls_inst._parsed_line.name + # if cls_inst.req and ( + # cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req + # ): + # cls_inst._parsed_line.ireq.req = cls_inst.req return cls_inst @classmethod @@ -2646,7 +2650,6 @@ def from_pipfile(cls, name, pipfile): if any(key in _pipfile for key in ["hash", "hashes"]): args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")]) cls_inst = cls(**args) - cls_inst.line_instance = Line(cls_inst.as_line()) return cls_inst def as_line( diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 59b0eacd..39eaa57f 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -48,7 +48,7 @@ if MYPY_RUNNING: - from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text + from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text, Set from pip_shims.shims import InstallRequirement, PackageFinder from pkg_resources import ( PathMetadata, DistInfoDistribution, Requirement as PkgResourcesRequirement @@ -67,6 +67,14 @@ _setup_distribution = None +class BuildEnv(pep517.envbuild.BuildEnvironment): + def pip_install(self, reqs): + cmd = [sys.executable, '-m', 'pip', 'install', '--ignore-installed', '--prefix', + self.path] + list(reqs) + run(cmd, block=True, combine_stderr=True, return_object=False, + write_to_stdout=False, nospin=True) + + @contextlib.contextmanager def _suppress_distutils_logs(): # type: () -> Generator[None, None, None] @@ -441,7 +449,7 @@ def get_setup_cfg(cls, setup_cfg_path): results["name"] = parser.get("metadata", "name") if parser.has_option("metadata", "version"): results["version"] = parser.get("metadata", "version") - install_requires = set() # type: Set(BaseRequirement) + install_requires = set() # type: Set[BaseRequirement] if parser.has_option("options", "install_requires"): install_requires = set([ BaseRequirement.from_string(dep) @@ -481,7 +489,7 @@ def egg_base(self): if base is None: base = Path(self.base_dir) if base is None: - base = Path(self.extra_kwargs["build_dir"]) + base = Path(self.extra_kwargs["src_dir"]) egg_base = base.joinpath("reqlib-metadata") if not egg_base.exists(): atexit.register(rmtree, egg_base.as_posix()) @@ -589,21 +597,25 @@ def run_setup(self): @contextlib.contextmanager def run_pep517(self): # type: (bool) -> Generator[pep517.wrappers.Pep517HookCaller, None, None] - with pep517.envbuild.BuildEnvironment(): - hookcaller = pep517.wrappers.Pep517HookCaller( - self.base_dir, self.build_backend - ) - hookcaller._subprocess_runner = pep517_subprocess_runner - build_deps = hookcaller.get_requires_for_build_wheel() - if self.ireq.editable: - build_deps += hookcaller.get_requires_for_build_sdist() - metadata_dirname = hookcaller.prepare_metadata_for_build_wheel(self.egg_base) - metadata_dir = os.path.join(self.egg_base, metadata_dirname) + config = {} + config.setdefault("--global-option", []) + builder = pep517.wrappers.Pep517HookCaller( + self.base_dir, self.build_backend + ) + builder._subprocess_runner = pep517_subprocess_runner + with BuildEnv() as env: + env.pip_install(self.build_requires) try: - yield hookcaller + reqs = builder.get_requires_for_build_wheel(config_settings=config) + env.pip_install(reqs) + metadata_dirname = builder.prepare_metadata_for_build_wheel( + self.egg_base, config_settings=config + ) except Exception: - build_deps = ["setuptools", "wheel"] - self.build_requires = tuple(set(self.build_requires) | set(build_deps)) + reqs = builder.get_requires_for_build_sdist(config_settings=config) + env.pip_install(reqs) + metadata_dir = os.path.join(self.egg_base, metadata_dirname) + yield builder def build(self): # type: () -> Optional[Text] @@ -711,9 +723,10 @@ def run_pyproject(self): self.build_backend = backend else: self.build_backend = "setuptools.build_meta:__legacy__" + if requires: + self.build_requires = tuple(set(requires) | set(self.build_requires)) + else: self.build_requires = ("setuptools", "wheel") - if requires and not self.build_requires: - self.build_requires = tuple(requires) def get_info(self): # type: () -> Dict[Text, Any] @@ -791,12 +804,8 @@ def from_ireq(cls, ireq, subdir=None, finder=None): if "file:/" in uri and "file:///" not in uri: uri = uri.replace("file:/", "file:///") path = pip_shims.shims.url_to_path(uri) - # if pip_shims.shims.is_installable_dir(path) and ireq.editable: - # ireq.source_dir = path kwargs = _prepare_wheel_building_kwargs(ireq) ireq.source_dir = kwargs["src_dir"] - # os.environ["PIP_BUILD_DIR"] = kwargs["build_dir"] - ireq.ensure_has_source_dir(kwargs["build_dir"]) if not ( ireq.editable and pip_shims.shims.is_file_url(ireq.link) @@ -813,17 +822,18 @@ def from_ireq(cls, ireq, subdir=None, finder=None): "The file URL points to a directory not installable: {}" .format(ireq.link) ) - if not ireq.editable: - build_dir = ireq.build_location(kwargs["build_dir"]) - ireq._temp_build_dir.path = kwargs["build_dir"] - else: - build_dir = ireq.build_location(kwargs["src_dir"]) - ireq._temp_build_dir.path = kwargs["build_dir"] + # if not ireq.editable: + build_dir = ireq.build_location(kwargs["build_dir"]) + src_dir = ireq.ensure_has_source_dir(kwargs["src_dir"]) + ireq._temp_build_dir.path = kwargs["build_dir"] + # else: + # build_dir = ireq.build_location(kwargs["src_dir"]) + # ireq._temp_build_dir.path = kwargs["build_dir"] ireq.populate_link(finder, False, False) pip_shims.shims.unpack_url( ireq.link, - build_dir, + src_dir, download_dir, only_download=only_download, session=finder.session, @@ -831,7 +841,7 @@ def from_ireq(cls, ireq, subdir=None, finder=None): progress_bar="off", ) created = cls.create( - build_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs + src_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs ) return created diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index f49e9e7c..eccf5e66 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -342,8 +342,10 @@ def get_pyproject(path): pp_toml = path.joinpath("pyproject.toml") setup_py = path.joinpath("setup.py") if not pp_toml.exists(): - if setup_py.exists(): + if not setup_py.exists(): return None + requires = ["setuptools>=40.6", "wheel"] + backend = "setuptools.build_meta:__legacy__" else: pyproject_data = {} with io.open(pp_toml.as_posix(), encoding="utf-8") as fh: @@ -351,10 +353,10 @@ def get_pyproject(path): build_system = pyproject_data.get("build-system", None) if build_system is None: if setup_py.exists(): - requires = ["setuptools", "wheel"] - backend = "setuptools.build_meta" + requires = ["setuptools>=40.6", "wheel"] + backend = "setuptools.build_meta:__legacy__" else: - requires = ["setuptools>=38.2.5", "wheel"] + requires = ["setuptools>=40.6", "wheel"] backend = "setuptools.build_meta" build_system = { "requires": requires, @@ -362,9 +364,9 @@ def get_pyproject(path): } pyproject_data["build_system"] = build_system else: - requires = build_system.get("requires") - backend = build_system.get("build-backend", "setuptools.build_meta") - return (requires, backend) + requires = build_system.get("requires", ["setuptools>=40.6", "wheel"]) + backend = build_system.get("build-backend", "setuptools.build_meta:__legacy__") + return (requires, backend) def split_markers_from_line(line): diff --git a/tasks/__init__.py b/tasks/__init__.py index 0639287e..3a219558 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -200,7 +200,7 @@ def clean_mdchangelog(ctx): def log(task, message, level=LogLevel.INFO): message_format = f"[{level.name.upper()}] " - if level >= LogLevel.ERROR: + if level.value >= LogLevel.ERROR.value: task = f"****** ({task}) " else: task = f"({task}) " From 175016455fff15d0427a5f692d15a0923ddaa021 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 15 Feb 2019 02:09:35 -0500 Subject: [PATCH 16/35] Fix wheel building and metadata retrieval Signed-off-by: Dan Ryan --- setup.cfg | 1 + src/requirementslib/models/requirements.py | 16 +- src/requirementslib/models/setup_info.py | 221 +++++++++++++++------ tests/unit/test_setup_info.py | 12 +- 4 files changed, 180 insertions(+), 70 deletions(-) diff --git a/setup.cfg b/setup.cfg index c9d2ed17..a02d9cc0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ setup_requires = install_requires = appdirs attrs + cached_property colorama distlib>=0.2.8 first diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index b25a6e1c..70280dea 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -22,13 +22,14 @@ import vistir from first import first +from cached_property import cached_property from packaging.markers import Marker from packaging.requirements import Requirement as PackagingRequirement from packaging.specifiers import Specifier, SpecifierSet, LegacySpecifier, InvalidSpecifier from packaging.utils import canonicalize_name from six.moves.urllib import parse as urllib_parse from six.moves.urllib.parse import unquote -from vistir.compat import Path, Iterable, FileNotFoundError +from vistir.compat import Path, Iterable, FileNotFoundError, lru_cache from vistir.contextmanagers import temp_path from vistir.misc import dedup from vistir.path import ( @@ -213,6 +214,8 @@ def line_for_ireq(self): if DIRECT_URL_RE.match(self.line): self._requirement = init_requirement(self.line) line = convert_direct_url_to_url(self.line) + else: + line = self.link.url if self.editable: if not line: @@ -1040,8 +1043,8 @@ def parse(self): "Supplied requirement is not installable: {0!r}".format(self.line) ) self.parse_link() - self.parse_requirement() - self.parse_ireq() + # self.parse_requirement() + # self.parse_ireq() @attr.s(slots=True, hash=True) @@ -2404,7 +2407,7 @@ def extras_as_pip(self): return "" - @property + @cached_property def commit_hash(self): # type: () -> Optional[Text] if not self.is_vcs: @@ -2443,7 +2446,7 @@ def update_name_from_path(self, path): def line_instance(self): # type: () -> Optional[Line] if self._line_instance is None: - if self.req.parsed_line is not None: + if self.req._parsed_line is not None: self._line_instance = self.req.parsed_line else: include_extras = True @@ -2546,7 +2549,7 @@ def is_wheel(self): return True return False - @property + @cached_property def normalized_name(self): # type: () -> Text return canonicalize_name(self.name) @@ -2555,6 +2558,7 @@ def copy(self): return attr.evolve(self) @classmethod + @lru_cache() def from_line(cls, line): # type: (Text) -> Requirement if isinstance(line, pip_shims.shims.InstallRequirement): diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 39eaa57f..68c97700 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -15,12 +15,13 @@ import pep517.wrappers import six from appdirs import user_cache_dir +from cached_property import cached_property from distlib.wheel import Wheel from packaging.markers import Marker from six.moves import configparser from six.moves.urllib.parse import unquote, urlparse, urlunparse -from vistir.compat import Iterable, Path +from vistir.compat import Iterable, Path, lru_cache from vistir.contextmanagers import cd, temp_path from vistir.misc import run from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p, rmtree @@ -67,6 +68,17 @@ _setup_distribution = None +def pep517_subprocess_runner(cmd, cwd=None, extra_environ=None): + # type: (List[Text], Optional[Text], Optional[Dict[Text, Text]]) -> None + """The default method of calling the wrapper subprocess.""" + env = os.environ.copy() + if extra_environ: + env.update(extra_environ) + + run(cmd, cwd=cwd, env=env, block=True, combine_stderr=True, return_object=False, + write_to_stdout=False, nospin=True) + + class BuildEnv(pep517.envbuild.BuildEnvironment): def pip_install(self, reqs): cmd = [sys.executable, '-m', 'pip', 'install', '--ignore-installed', '--prefix', @@ -75,6 +87,13 @@ def pip_install(self, reqs): write_to_stdout=False, nospin=True) +class HookCaller(pep517.wrappers.Pep517HookCaller): + def __init__(self, source_dir, build_backend): + self.source_dir = os.path.abspath(source_dir) + self.build_backend = build_backend + self._subprocess_runner = pep517_subprocess_runner + + @contextlib.contextmanager def _suppress_distutils_logs(): # type: () -> Generator[None, None, None] @@ -95,6 +114,25 @@ def _log(log, level, msg, args): distutils.log.Log._log = f +def build_pep517(source_dir, build_dir, config_settings=None, dist_type="wheel"): + if config_settings is None: + config_settings = {} + requires, backend = get_pyproject(source_dir) + hookcaller = HookCaller(source_dir, backend) + if dist_type == "sdist": + get_requires_fn = hookcaller.get_requires_for_build_sdist + build_fn = hookcaller.build_sdist + else: + get_requires_fn = hookcaller.get_requires_for_build_wheel + build_fn = hookcaller.build_wheel + + with BuildEnv() as env: + env.pip_install(requires) + reqs = get_requires_fn(config_settings) + env.pip_install(reqs) + return build_fn(build_dir, config_settings) + + @ensure_mkdir_p(mode=0o775) def _get_src_dir(root): # type: (Text) -> Text @@ -112,6 +150,7 @@ def _get_src_dir(root): return src_dir +@lru_cache() def ensure_reqs(reqs): # type: (List[Union[Text, PkgResourcesRequirement]]) -> List[PkgResourcesRequirement] import pkg_resources @@ -128,17 +167,6 @@ def ensure_reqs(reqs): return new_reqs -def pep517_subprocess_runner(cmd, cwd=None, extra_environ=None): - # type: (List[Text], Optional[Text], Optional[Dict[Text, Text]]) -> None - """The default method of calling the wrapper subprocess.""" - env = os.environ.copy() - if extra_environ: - env.update(extra_environ) - - run(cmd, cwd=cwd, env=env, block=True, combine_stderr=True, return_object=False, - write_to_stdout=False, nospin=True) - - def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, editable=False): # type: (Optional[InstallRequirement], Optional[Text], Optional[Text], bool) -> Dict[Text, Text] download_dir = os.path.join(CACHE_DIR, "pkgs") # type: Text @@ -249,6 +277,7 @@ def get_metadata(path, pkg_name=None, metadata_type=None): return {} +@lru_cache() def get_extra_name_from_marker(marker): # type: (MarkerType) -> Optional[Text] if not marker: @@ -322,7 +351,7 @@ def get_metadata_from_dist(dist): marker = "" extra = "{0}".format(k) _deps = ["{0}{1}".format(str(req), marker) for req in _deps] - _deps = ensure_reqs(_deps) + _deps = ensure_reqs(tuple(_deps)) if extra: extras[extra] = _deps else: @@ -353,6 +382,7 @@ def as_tuple(self): return (self.name, self.requirement) @classmethod + @lru_cache() def from_string(cls, line): # type: (Text) -> BaseRequirement line = line.strip() @@ -360,6 +390,7 @@ def from_string(cls, line): return cls.from_req(req) @classmethod + @lru_cache() def from_req(cls, req): # type: (PkgResourcesRequirement) -> BaseRequirement name = None @@ -594,62 +625,116 @@ def run_setup(self): if not self.version: self.version = dist.get_version() - @contextlib.contextmanager - def run_pep517(self): - # type: (bool) -> Generator[pep517.wrappers.Pep517HookCaller, None, None] + @property + @lru_cache() + def pep517_config(self): config = {} config.setdefault("--global-option", []) - builder = pep517.wrappers.Pep517HookCaller( - self.base_dir, self.build_backend + return config + + def build_wheel(self): + # type: () -> Text + if not self.pyproject.exists(): + build_requires = ", ".join(['"{0}"'.format(r) for r in self.build_requires]) + self.pyproject.write_text(u""" +[build-system] +requires = [{0}] +build-backend = "{1}" + """.format(build_requires, self.build_backend).strip()) + return build_pep517( + self.base_dir, self.extra_kwargs["build_dir"], + config_settings=self.pep517_config, + dist_type="wheel" ) - builder._subprocess_runner = pep517_subprocess_runner - with BuildEnv() as env: - env.pip_install(self.build_requires) - try: - reqs = builder.get_requires_for_build_wheel(config_settings=config) - env.pip_install(reqs) - metadata_dirname = builder.prepare_metadata_for_build_wheel( - self.egg_base, config_settings=config - ) - except Exception: - reqs = builder.get_requires_for_build_sdist(config_settings=config) - env.pip_install(reqs) - metadata_dir = os.path.join(self.egg_base, metadata_dirname) - yield builder + + def build_sdist(self): + # type: () -> Text + if not self.pyproject.exists(): + build_requires = ", ".join(['"{0}"'.format(r) for r in self.build_requires]) + self.pyproject.write_text(u""" +[build-system] +requires = [{0}] +build-backend = "{1}" + """.format(build_requires, self.build_backend).strip()) + return build_pep517( + self.base_dir, self.extra_kwargs["build_dir"], + config_settings=self.pep517_config, + dist_type="sdist" + ) + + # @contextlib.contextmanager + # def run_pep517(self): + # # type: (bool) -> Generator[pep517.wrappers.Pep517HookCaller, None, None] + # builder = pep517.wrappers.Pep517HookCaller( + # self.base_dir, self.build_backend + # ) + # builder._subprocess_runner = pep517_subprocess_runner + # with BuildEnv() as env: + # env.pip_install(self.build_requires) + # try: + # reqs = builder.get_requires_for_build_wheel(config_settings=self.pep517_config) + # env.pip_install(reqs) + # metadata_dirname = builder.prepare_metadata_for_build_wheel( + # self.egg_base, config_settings=self.pep517_config + # ) + # except Exception: + # reqs = builder.get_requires_for_build_sdist(config_settings=self.pep517_config) + # env.pip_install(reqs) + # metadata_dir = os.path.join(self.egg_base, metadata_dirname) + # yield builder def build(self): # type: () -> Optional[Text] dist_path = None - with self.run_pep517() as hookcaller: - dist_path = self.build_pep517(hookcaller) - if os.path.exists(os.path.join(self.extra_kwargs["build_dir"], dist_path)): - self.get_metadata_from_wheel( - os.path.join(self.extra_kwargs["build_dir"], dist_path) - ) - if not self.metadata or not self.name: - self.get_egg_metadata() - else: - return dist_path - if not self.metadata or not self.name: - hookcaller._subprocess_runner( - ["setup.py", "egg_info", "--egg-base", self.egg_base] - ) - self.get_egg_metadata() - return dist_path - - def build_pep517(self, hookcaller): - # type: (pep517.wrappers.Pep517HookCaller) -> Optional[Text] - dist_path = None try: - dist_path = hookcaller.build_wheel( - self.extra_kwargs["build_dir"], - metadata_directory=self.egg_base - ) - return dist_path + dist_path = self.build_wheel() except Exception: - dist_path = hookcaller.build_sdist(self.extra_kwargs["build_dir"]) - self.get_egg_metadata(metadata_type="egg") - return dist_path + try: + dist_path = self.build_sdist() + self.get_egg_metadata(metadata_type="egg") + except Exception: + pass + else: + self.get_metadata_from_wheel( + os.path.join(self.extra_kwargs["build_dir"], dist_path) + ) + if not self.metadata or not self.name: + self.get_egg_metadata() + if not self.metadata or not self.name: + self.run_setup() + # with self.run_pep517() as hookcaller: + # dist_path = self.build_pep517(hookcaller) + # if os.path.exists(os.path.join(self.extra_kwargs["build_dir"], dist_path)): + # self.get_metadata_from_wheel( + # os.path.join(self.extra_kwargs["build_dir"], dist_path) + # ) + # if not self.metadata or not self.name: + # self.get_egg_metadata() + # else: + # return dist_path + # if not self.metadata or not self.name: + # hookcaller._subprocess_runner( + # ["setup.py", "egg_info", "--egg-base", self.egg_base] + # ) + # self.get_egg_metadata() + # return dist_path + + # def build_pep517(self, hookcaller): + # # type: (pep517.wrappers.Pep517HookCaller) -> Optional[Text] + # dist_path = None + # try: + # dist_path = hookcaller.build_wheel( + # self.extra_kwargs["build_dir"], + # metadata_directory=self.egg_base, + # config_settings=self.pep517_config + # ) + # return dist_path + # except Exception: + # dist_path = hookcaller.build_sdist( + # self.extra_kwargs["build_dir"], config_settings=self.pep517_config + # ) + # self.get_egg_metadata(metadata_type="egg") + # return dist_path def reload(self): # type: () -> Dict[Text, Any] @@ -688,7 +773,18 @@ def get_egg_metadata(self, metadata_dir=None, metadata_type=None): def populate_metadata(self, metadata): # type: (Dict[Any, Any]) -> None - self.metadata = tuple([(k, v) for k, v in metadata.items()]) + _metadata = () + for k, v in metadata.items(): + if k == "extras" and isinstance(v, dict): + extras = () + for extra, reqs in v.items(): + extras += ((extra, tuple(reqs)),) + _metadata += extras + elif isinstance(v, (list, tuple)): + _metadata += (k, tuple(v)) + else: + _metadata += (k, v) + self.metadata = _metadata if self.name is None: self.name = metadata.get("name", self.name) if not self.version: @@ -705,7 +801,7 @@ def populate_metadata(self, metadata): if extras: extras_tuple = tuple([ BaseRequirement.from_req(req) - for req in ensure_reqs(extras) + for req in ensure_reqs(tuple(extras)) if req is not None ]) self._extras_requirements += ((extra, extras_tuple),) @@ -780,6 +876,7 @@ def from_requirement(cls, requirement, finder=None): return cls.from_ireq(ireq, subdir=subdir, finder=finder) @classmethod + @lru_cache() def from_ireq(cls, ireq, subdir=None, finder=None): # type: (InstallRequirement, Optional[Text], Optional[PackageFinder]) -> Optional[SetupInfo] import pip_shims.shims diff --git a/tests/unit/test_setup_info.py b/tests/unit/test_setup_info.py index 0e6f6cce..5468585b 100644 --- a/tests/unit/test_setup_info.py +++ b/tests/unit/test_setup_info.py @@ -48,13 +48,21 @@ def find_metadata(path): metadata_names = [ os.path.join(path, name) for name in (egg_info_name, distinfo_name) ] - return next(iter(pth for pth in metadata_names if os.path.isdir(pth)), None) + if not os.path.isdir(path): + return None + pth = next(iter(pth for pth in metadata_names if os.path.isdir(pth)), None) + if not pth: + pth = next(iter( + pth for pth in os.listdir(path) + if any(pth.endswith(md_ending) for md_ending in [".egg-info", ".dist-info"]) + ), None) + return pth assert not find_metadata(base_dir) assert not find_metadata(os.path.join(base_dir, "reqlib-metadata")) assert not find_metadata(os.path.join(base_dir, "src", "reqlib-metadata")) assert r.req.setup_info and os.path.isdir(r.req.setup_info.egg_base) - assert find_metadata(r.req.setup_info.egg_base) + assert find_metadata(r.req.setup_info.egg_base) or find_metadata(r.req.setup_info.extra_kwargs["build_dir"]) def test_without_extras(pathlib_tmpdir): From 7840a8346558a6a4bc657b4bb690c6bc30285f78 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 15 Feb 2019 21:51:54 -0500 Subject: [PATCH 17/35] Chip away at old/unused code - Functionality is all superseded, remove commented sections Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 40 ++------------- src/requirementslib/models/setup_info.py | 58 ---------------------- 2 files changed, 4 insertions(+), 94 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 70280dea..369e8335 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -6,8 +6,6 @@ import copy import hashlib import os -import re -import string import sys from distutils.sysconfig import get_python_lib @@ -15,8 +13,6 @@ from functools import partial import attr -import pep517 -import pep517.wrappers import pip_shims import six import vistir @@ -29,7 +25,7 @@ from packaging.utils import canonicalize_name from six.moves.urllib import parse as urllib_parse from six.moves.urllib.parse import unquote -from vistir.compat import Path, Iterable, FileNotFoundError, lru_cache +from vistir.compat import Path, FileNotFoundError, lru_cache from vistir.contextmanagers import temp_path from vistir.misc import dedup from vistir.path import ( @@ -46,7 +42,6 @@ VCS_LIST, is_installable_file, is_vcs, - ensure_setup_py, add_ssh_scheme_to_git_uri, strip_ssh_from_git_uri, get_setup_paths @@ -74,7 +69,6 @@ create_link, get_pyproject, convert_direct_url_to_url, - convert_url_to_direct_url, URL_RE, DIRECT_URL_RE ) @@ -1669,13 +1663,6 @@ def create( if name: creation_kwargs["name"] = name cls_inst = cls(**creation_kwargs) # type: ignore - # if parsed_line and not cls_inst._parsed_line: - # cls_inst._parsed_line = parsed_line - # if not cls_inst._parsed_line: - # cls_inst._parsed_line = Line(cls_inst.line_part) - # if cls_inst._parsed_line and cls_inst.parsed_line.ireq and not cls_inst.parsed_line.ireq.req: - # if cls_inst.req: - # cls_inst._parsed_line._ireq.req = cls_inst.req return cls_inst @classmethod @@ -1909,10 +1896,6 @@ def __attrs_post_init__(self): new_uri = urllib_parse.urlunsplit((scheme,) + rest[:-1] + ("",)) new_uri = "{0}{1}".format(vcs_type, new_uri) self.uri = new_uri - # if self.req and self._parsed_line and ( - # self._parsed_line.ireq and not self._parsed_line.ireq.req - # ): - # self._parsed_line._ireq.req = self.req @link.default def get_link(self): @@ -2134,10 +2117,10 @@ def locked_vcs_repo(self, src_dir=None): if self.req: self.req.specifier = SpecifierSet("=={0}".format(self.setup_info.version)) try: - yield vcsrepo - finally: + yield self._repo + except Exception: self._repo = orig_repo - # self._setup_info = _old_setup_info + raise @classmethod def from_pipfile(cls, name, pipfile): @@ -2185,21 +2168,6 @@ def from_pipfile(cls, name, pipfile): creation_args[key] = pipfile.get(key) creation_args["name"] = name cls_inst = cls(**creation_args) - # if cls_inst._parsed_line is None: - # vcs_uri = build_vcs_uri( - # vcs=cls_inst.vcs, uri=add_ssh_scheme_to_git_uri(cls_inst.uri), - # name=cls_inst.name, ref=cls_inst.ref, subdirectory=cls_inst.subdirectory, - # extras=cls_inst.extras - # ) - # if cls_inst.editable: - # vcs_uri = "-e {0}".format(vcs_uri) - # cls_inst._parsed_line = Line(vcs_uri) - # if not cls_inst.name and cls_inst._parsed_line.name: - # cls_inst.name = cls_inst._parsed_line.name - # if cls_inst.req and ( - # cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req - # ): - # cls_inst._parsed_line.ireq.req = cls_inst.req return cls_inst @classmethod diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 68c97700..51bf9044 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -662,27 +662,6 @@ def build_sdist(self): dist_type="sdist" ) - # @contextlib.contextmanager - # def run_pep517(self): - # # type: (bool) -> Generator[pep517.wrappers.Pep517HookCaller, None, None] - # builder = pep517.wrappers.Pep517HookCaller( - # self.base_dir, self.build_backend - # ) - # builder._subprocess_runner = pep517_subprocess_runner - # with BuildEnv() as env: - # env.pip_install(self.build_requires) - # try: - # reqs = builder.get_requires_for_build_wheel(config_settings=self.pep517_config) - # env.pip_install(reqs) - # metadata_dirname = builder.prepare_metadata_for_build_wheel( - # self.egg_base, config_settings=self.pep517_config - # ) - # except Exception: - # reqs = builder.get_requires_for_build_sdist(config_settings=self.pep517_config) - # env.pip_install(reqs) - # metadata_dir = os.path.join(self.egg_base, metadata_dirname) - # yield builder - def build(self): # type: () -> Optional[Text] dist_path = None @@ -702,39 +681,6 @@ def build(self): self.get_egg_metadata() if not self.metadata or not self.name: self.run_setup() - # with self.run_pep517() as hookcaller: - # dist_path = self.build_pep517(hookcaller) - # if os.path.exists(os.path.join(self.extra_kwargs["build_dir"], dist_path)): - # self.get_metadata_from_wheel( - # os.path.join(self.extra_kwargs["build_dir"], dist_path) - # ) - # if not self.metadata or not self.name: - # self.get_egg_metadata() - # else: - # return dist_path - # if not self.metadata or not self.name: - # hookcaller._subprocess_runner( - # ["setup.py", "egg_info", "--egg-base", self.egg_base] - # ) - # self.get_egg_metadata() - # return dist_path - - # def build_pep517(self, hookcaller): - # # type: (pep517.wrappers.Pep517HookCaller) -> Optional[Text] - # dist_path = None - # try: - # dist_path = hookcaller.build_wheel( - # self.extra_kwargs["build_dir"], - # metadata_directory=self.egg_base, - # config_settings=self.pep517_config - # ) - # return dist_path - # except Exception: - # dist_path = hookcaller.build_sdist( - # self.extra_kwargs["build_dir"], config_settings=self.pep517_config - # ) - # self.get_egg_metadata(metadata_type="egg") - # return dist_path def reload(self): # type: () -> Dict[Text, Any] @@ -919,13 +865,9 @@ def from_ireq(cls, ireq, subdir=None, finder=None): "The file URL points to a directory not installable: {}" .format(ireq.link) ) - # if not ireq.editable: build_dir = ireq.build_location(kwargs["build_dir"]) src_dir = ireq.ensure_has_source_dir(kwargs["src_dir"]) ireq._temp_build_dir.path = kwargs["build_dir"] - # else: - # build_dir = ireq.build_location(kwargs["src_dir"]) - # ireq._temp_build_dir.path = kwargs["build_dir"] ireq.populate_link(finder, False, False) pip_shims.shims.unpack_url( From ab3267c766c897fb129c9c42b691a71bcd5b82d7 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 15 Feb 2019 21:52:42 -0500 Subject: [PATCH 18/35] Update url to tablib zipfile Signed-off-by: Dan Ryan --- tests/unit/test_requirements.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py index 989b2668..830b8f97 100644 --- a/tests/unit/test_requirements.py +++ b/tests/unit/test_requirements.py @@ -248,9 +248,9 @@ def test_get_requirements(monkeypatch): assert url_with_egg.name == 'django-user-clipboard' # Test URLs without eggs pointing at installable zipfiles url = Requirement.from_line( - 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip' + 'https://codeload.github.com/kennethreitz/tablib/zip/v0.12.1' ).requirement - assert url.url == 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip' + assert url.url == 'https://codeload.github.com/kennethreitz/tablib/zip/v0.12.1' wheel_line = "https://github.com/pypa/pipenv/raw/master/tests/test_artifacts/six-1.11.0+mkl-py2.py3-none-any.whl" wheel = Requirement.from_line(wheel_line) assert wheel.as_pipfile() == { @@ -334,11 +334,11 @@ def test_local_editable_ref(monkeypatch): def test_pep_508(): - r = Requirement.from_line("tablib@ https://github.com/kennethreitz/tablib/archive/v0.12.1.zip") + r = Requirement.from_line("tablib@ https://codeload.github.com/kennethreitz/tablib/zip/v0.12.1") assert r.specifiers == "==0.12.1" - assert r.req.link.url == "https://github.com/kennethreitz/tablib/archive/v0.12.1.zip#egg=tablib" + assert r.req.link.url == "https://codeload.github.com/kennethreitz/tablib/zip/v0.12.1#egg=tablib" assert r.req.req.name == "tablib" - assert r.req.req.url == "https://github.com/kennethreitz/tablib/archive/v0.12.1.zip" + assert r.req.req.url == "https://codeload.github.com/kennethreitz/tablib/zip/v0.12.1" requires, setup_requires, build_requires = r.req.dependencies assert all(dep in requires for dep in ["openpyxl", "odfpy", "xlrd"]) - assert r.as_pipfile() == {'tablib': {'file': 'https://github.com/kennethreitz/tablib/archive/v0.12.1.zip'}} + assert r.as_pipfile() == {'tablib': {'file': 'https://codeload.github.com/kennethreitz/tablib/zip/v0.12.1'}} From 717a33dc25c8ae7f119b8e77283b24c882537a04 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 15 Feb 2019 21:56:00 -0500 Subject: [PATCH 19/35] Updated news entry Signed-off-by: Dan Ryan --- news/108.feature.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/news/108.feature.rst b/news/108.feature.rst index 0e8df2cf..feb1b8b0 100644 --- a/news/108.feature.rst +++ b/news/108.feature.rst @@ -1 +1,3 @@ -Provide support for parsing PEP-508 compliant direct URL dependencies. +Added full support for parsing PEP-508 compliant direct URL dependencies. + +Fully implemented pep517 dependency mapping for VCS, URL, and file-type requirements. From 396de7bfca4ad1c0bfcfc99859b46addbbe86946 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sun, 17 Feb 2019 23:28:07 -0500 Subject: [PATCH 20/35] adjust pep517 builder for older setuptools Signed-off-by: Dan Ryan --- setup.cfg | 4 +- src/requirementslib/models/pipfile.py | 4 +- src/requirementslib/models/requirements.py | 67 ++++++++++++++++++---- src/requirementslib/models/setup_info.py | 13 +++-- src/requirementslib/models/utils.py | 38 +++++++++--- 5 files changed, 99 insertions(+), 27 deletions(-) diff --git a/setup.cfg b/setup.cfg index a02d9cc0..768f9f33 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* setup_requires = invoke vistir - setuptools>=38.2.5 + setuptools>=40.8 wheel parver install_requires = @@ -58,7 +58,7 @@ install_requires = plette[validation] requests scandir;python_version<"3.5" - setuptools>=38.2.5 + setuptools>=40.8 six tomlkit>=0.5.2 typing;python_version<"3.5" diff --git a/src/requirementslib/models/pipfile.py b/src/requirementslib/models/pipfile.py index 021b5d53..ba7eeed5 100644 --- a/src/requirementslib/models/pipfile.py +++ b/src/requirementslib/models/pipfile.py @@ -340,8 +340,8 @@ def _read_pyproject(self): if not os.path.exists(self.path_to("setup.py")): if not build_system or not build_system.get("requires"): build_system = { - "requires": ["setuptools>=38.2.5", "wheel"], - "build-backend": "setuptools.build_meta", + "requires": ["setuptools>=40.8", "wheel"], + "build-backend": "setuptools.build_meta:__legacy__", } self._build_system = build_system diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 369e8335..18b83195 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -70,7 +70,8 @@ get_pyproject, convert_direct_url_to_url, URL_RE, - DIRECT_URL_RE + DIRECT_URL_RE, + get_default_pyproject_backend ) from ..environment import MYPY_RUNNING @@ -305,7 +306,11 @@ def specifiers(self): # note: we need versions for direct dependencies at the very least if self.is_file or self.is_url or self.is_path or (self.is_vcs and not self.editable): if self.specifier is not None: - self.specifiers = self.specifier + specifier = self.specifier + if not isinstance(specifier, SpecifierSet): + specifier = SpecifierSet(specifier) + self.specifiers = specifier + return specifier if self.ireq is not None and self.ireq.req is not None: return self.ireq.req.specifier elif self.requirement is not None: @@ -381,7 +386,7 @@ def pyproject_backend(self): pyproject_requires, pyproject_backend = get_pyproject(self.path) if not pyproject_backend and self.setup_cfg is not None: setup_dict = SetupInfo.get_setup_cfg(self.setup_cfg) - pyproject_backend = "setuptools.build_meta:__legacy__" + pyproject_backend = get_default_pyproject_backend() pyproject_requires = setup_dict.get("build_requires", ["setuptools", "wheel"]) self._pyproject_requires = pyproject_requires @@ -931,6 +936,8 @@ def parse_link(self): self.relpath = relpath self.path = path self.uri = uri + if prefer in ("path", "relpath") or uri.startswith("file"): + self.is_local = True if link.egg_fragment: name, extras = pip_shims.shims._strip_extras(link.egg_fragment) self.extras = tuple(sorted(set(parse_extras(extras)))) @@ -1819,7 +1826,14 @@ def pipfile_part(self): ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() - name = pipfile_dict.pop("name") + name = pipfile_dict.pop("name", None) + if name is None: + if self.name: + name = self.name + elif self.parsed_line and self.parsed_line.name: + name = self.name = self.parsed_line.name + elif self.setup_info and self.setup_info.name: + name = self.name = self.setup_info.name if "_uri_scheme" in pipfile_dict: pipfile_dict.pop("_uri_scheme") # For local paths and remote installable artifacts (zipfiles, etc) @@ -2302,15 +2316,23 @@ def pipfile_part(self): ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() + name = pipfile_dict.pop("name", None) + if name is None: + if self.name: + name = self.name + elif self.parsed_line and self.parsed_line.name: + name = self.name = self.parsed_line.name + elif self.setup_info and self.setup_info.name: + name = self.name = self.setup_info.name if "vcs" in pipfile_dict: pipfile_dict = self._choose_vcs_source(pipfile_dict) - name, _ = pip_shims.shims._strip_extras(pipfile_dict.pop("name")) + name, _ = pip_shims.shims._strip_extras(name) return {name: pipfile_dict} @attr.s(cmp=True, hash=True) class Requirement(object): - name = attr.ib(cmp=True) # type: Text + _name = attr.ib(cmp=True) # type: Text vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[Text] req = attr.ib(default=None, cmp=True) # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] markers = attr.ib(default=None, cmp=True) # type: Optional[Text] @@ -2326,11 +2348,24 @@ class Requirement(object): def __hash__(self): return hash(self.as_line()) - @name.default + @_name.default def get_name(self): # type: () -> Optional[Text] return self.req.name + @property + def name(self): + # type: () -> Optional[Text] + if self._name is not None: + return self._name + name = None + if self.req and self.req.name: + name = self.req.name + elif self.req and self.is_file_or_url and self.req.setup_info: + name = self.req.setup_info.name + self._name = name + return name + @property def requirement(self): # type: () -> Optional[PackagingRequirement] @@ -2517,7 +2552,7 @@ def is_wheel(self): return True return False - @cached_property + @property def normalized_name(self): # type: () -> Text return canonicalize_name(self.name) @@ -2661,7 +2696,9 @@ def as_line( parts.extend(hashes) else: parts.append(hashes) - if sources and not (self.requirement.local_file or self.vcs): + + is_local = self.is_file_or_url and self.req and self.req.is_local + if sources and self.requirement and not (is_local or self.vcs): from ..utils import prepare_pip_source_args if self.index: @@ -2735,9 +2772,17 @@ def as_pipfile(self): name = self.name if "markers" in req_dict and req_dict["markers"]: req_dict["markers"] = req_dict["markers"].replace('"', "'") + if not self.req.name: + name_carriers = (self.req, self, self.line_instance, self.req.parsed_line) + name_options = [ + getattr(carrier, "name", None) + for carrier in name_carriers if carrier is not None + ] + req_name = next(iter(n for n in name_options if n is not None), None) + self.req.name = req_name + req_name, dict_from_subreq = self.req.pipfile_part.popitem() base_dict = { - k: v - for k, v in self.req.pipfile_part[name].items() + k: v for k, v in dict_from_subreq.items() if k not in ["req", "link", "_setup_info"] } base_dict.update(req_dict) diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 51bf9044..5f2ac1db 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -33,7 +33,8 @@ get_pyproject, init_requirement, split_vcs_method_from_uri, - strip_extras_markers_from_requirement + strip_extras_markers_from_requirement, + get_default_pyproject_backend ) try: @@ -431,7 +432,7 @@ class SetupInfo(object): version = attr.ib(default=None, cmp=True) # type: Text _requirements = attr.ib(type=frozenset, factory=frozenset, cmp=True, hash=True) build_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) - build_backend = attr.ib(default="setuptools.build_meta:__legacy__", cmp=True) # type: Text + build_backend = attr.ib(cmp=True) # type: Text setup_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None, cmp=True) _extras_requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) @@ -442,6 +443,10 @@ class SetupInfo(object): extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict, cmp=False, hash=False) metadata = attr.ib(default=None) # type: Optional[Tuple[Text]] + @build_backend.default + def get_build_backend(self): + return get_default_pyproject_backend() + @property def requires(self): # type: () -> Dict[Text, RequirementType] @@ -713,7 +718,7 @@ def get_egg_metadata(self, metadata_dir=None, metadata_type=None): get_metadata(d, pkg_name=self.name, metadata_type=metadata_type) for d in metadata_dirs if os.path.exists(d) ] - metadata = next(iter(d for d in metadata if d is not None), None) + metadata = next(iter(d for d in metadata if d), None) if metadata is not None: self.populate_metadata(metadata) @@ -764,7 +769,7 @@ def run_pyproject(self): if backend: self.build_backend = backend else: - self.build_backend = "setuptools.build_meta:__legacy__" + self.build_backend = get_default_pyproject_backend() if requires: self.build_requires = tuple(set(requires) | set(self.build_requires)) else: diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index eccf5e66..6ad4972d 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -18,8 +18,10 @@ from first import first from packaging.markers import InvalidMarker, Marker, Op, Value, Variable from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet +from packaging.version import parse as parse_version from six.moves.urllib import parse as urllib_parse from urllib3 import util as urllib3_util +from vistir.compat import lru_cache from vistir.misc import dedup from vistir.path import is_valid_url @@ -321,6 +323,26 @@ def _strip_extras_markers(marker): return marker +@lru_cache() +def get_setuptools_version(): + # type: () -> Optional[Text] + import pkg_resources + setuptools_dist = pkg_resources.get_distribution( + pkg_resources.Requirement("setuptools") + ) + return getattr(setuptools_dist, "version", None) + + +def get_default_pyproject_backend(): + # type: () -> Text + st_version = get_setuptools_version() + if st_version is not None: + parsed_st_version = parse_version(st_version) + if parsed_st_version >= parse_version("40.8.0"): + return "setuptools.build_meta:__legacy__" + return "setuptools.build_meta" + + def get_pyproject(path): # type: (Union[Text, Path]) -> Tuple[List[Text], Text] """ @@ -344,8 +366,8 @@ def get_pyproject(path): if not pp_toml.exists(): if not setup_py.exists(): return None - requires = ["setuptools>=40.6", "wheel"] - backend = "setuptools.build_meta:__legacy__" + requires = ["setuptools>=40.8", "wheel"] + backend = get_default_pyproject_backend() else: pyproject_data = {} with io.open(pp_toml.as_posix(), encoding="utf-8") as fh: @@ -353,19 +375,19 @@ def get_pyproject(path): build_system = pyproject_data.get("build-system", None) if build_system is None: if setup_py.exists(): - requires = ["setuptools>=40.6", "wheel"] - backend = "setuptools.build_meta:__legacy__" + requires = ["setuptools>=40.8", "wheel"] + backend = get_default_pyproject_backend() else: - requires = ["setuptools>=40.6", "wheel"] - backend = "setuptools.build_meta" + requires = ["setuptools>=40.8", "wheel"] + backend = get_default_pyproject_backend() build_system = { "requires": requires, "build-backend": backend } pyproject_data["build_system"] = build_system else: - requires = build_system.get("requires", ["setuptools>=40.6", "wheel"]) - backend = build_system.get("build-backend", "setuptools.build_meta:__legacy__") + requires = build_system.get("requires", ["setuptools>=40.8", "wheel"]) + backend = build_system.get("build-backend", get_default_pyproject_backend()) return (requires, backend) From f035abfc6a0d81150f8cdd79bfb858c6b275fb45 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sun, 24 Feb 2019 18:08:19 -0500 Subject: [PATCH 21/35] Update news entry and towncrier config Signed-off-by: Dan Ryan --- news/108.feature.rst | 2 + pyproject.toml | 5 +- src/requirementslib/models/requirements.py | 59 ++++++++++++++++++---- src/requirementslib/models/setup_info.py | 11 ++-- src/requirementslib/models/utils.py | 38 +++++++++++--- 5 files changed, 93 insertions(+), 22 deletions(-) diff --git a/news/108.feature.rst b/news/108.feature.rst index feb1b8b0..a00a6932 100644 --- a/news/108.feature.rst +++ b/news/108.feature.rst @@ -1,3 +1,5 @@ Added full support for parsing PEP-508 compliant direct URL dependencies. Fully implemented pep517 dependency mapping for VCS, URL, and file-type requirements. + +Expanded type-checking coverage. diff --git a/pyproject.toml b/pyproject.toml index 65b4ffd6..73c3fa6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,14 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools>=40.8.0", "wheel>=0.33.0"] [tool.towncrier] package = "requirementslib" package_dir = "src" -filename = "CHANGELOG.rst" +newsfile = "CHANGELOG.rst" directory = "news/" title_format = "{version} ({project_date})" template = "tasks/CHANGELOG.rst.jinja2" +issue_format = '`#{issue} `_' [[tool.towncrier.type]] directory = "feature" diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 369e8335..d6c13819 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -70,7 +70,8 @@ get_pyproject, convert_direct_url_to_url, URL_RE, - DIRECT_URL_RE + DIRECT_URL_RE, + get_default_pyproject_backend ) from ..environment import MYPY_RUNNING @@ -305,7 +306,11 @@ def specifiers(self): # note: we need versions for direct dependencies at the very least if self.is_file or self.is_url or self.is_path or (self.is_vcs and not self.editable): if self.specifier is not None: - self.specifiers = self.specifier + specifier = self.specifier + if not isinstance(specifier, SpecifierSet): + specifier = SpecifierSet(specifier) + self.specifiers = specifier + return specifier if self.ireq is not None and self.ireq.req is not None: return self.ireq.req.specifier elif self.requirement is not None: @@ -381,7 +386,7 @@ def pyproject_backend(self): pyproject_requires, pyproject_backend = get_pyproject(self.path) if not pyproject_backend and self.setup_cfg is not None: setup_dict = SetupInfo.get_setup_cfg(self.setup_cfg) - pyproject_backend = "setuptools.build_meta:__legacy__" + pyproject_backend = get_default_pyproject_backend() pyproject_requires = setup_dict.get("build_requires", ["setuptools", "wheel"]) self._pyproject_requires = pyproject_requires @@ -1819,7 +1824,14 @@ def pipfile_part(self): ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() - name = pipfile_dict.pop("name") + name = pipfile_dict.pop("name", None) + if name is None: + if self.name: + name = self.name + elif self.parsed_line and self.parsed_line.name: + name = self.name = self.parsed_line.name + elif self.setup_info and self.setup_info.name: + name = self.name = self.setup_info.name if "_uri_scheme" in pipfile_dict: pipfile_dict.pop("_uri_scheme") # For local paths and remote installable artifacts (zipfiles, etc) @@ -2302,15 +2314,23 @@ def pipfile_part(self): ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa pipfile_dict = attr.asdict(self, filter=filter_func).copy() + name = pipfile_dict.pop("name", None) + if name is None: + if self.name: + name = self.name + elif self.parsed_line and self.parsed_line.name: + name = self.name = self.parsed_line.name + elif self.setup_info and self.setup_info.name: + name = self.name = self.setup_info.name if "vcs" in pipfile_dict: pipfile_dict = self._choose_vcs_source(pipfile_dict) - name, _ = pip_shims.shims._strip_extras(pipfile_dict.pop("name")) + name, _ = pip_shims.shims._strip_extras(name) return {name: pipfile_dict} @attr.s(cmp=True, hash=True) class Requirement(object): - name = attr.ib(cmp=True) # type: Text + _name = attr.ib(cmp=True) # type: Text vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[Text] req = attr.ib(default=None, cmp=True) # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] markers = attr.ib(default=None, cmp=True) # type: Optional[Text] @@ -2326,11 +2346,24 @@ class Requirement(object): def __hash__(self): return hash(self.as_line()) - @name.default + @_name.default def get_name(self): # type: () -> Optional[Text] return self.req.name + @property + def name(self): + # type: () -> Optional[Text] + if self._name is not None: + return self._name + name = None + if self.req and self.req.name: + name = self.req.name + elif self.req and self.is_file_or_url and self.req.setup_info: + name = self.req.setup_info.name + self._name = name + return name + @property def requirement(self): # type: () -> Optional[PackagingRequirement] @@ -2735,9 +2768,17 @@ def as_pipfile(self): name = self.name if "markers" in req_dict and req_dict["markers"]: req_dict["markers"] = req_dict["markers"].replace('"', "'") + if not self.req.name: + name_carriers = (self.req, self, self.line_instance, self.req.parsed_line) + name_options = [ + getattr(carrier, "name", None) + for carrier in name_carriers if carrier is not None + ] + req_name = next(iter(n for n in name_options if n is not None), None) + self.req.name = req_name + req_name, dict_from_subreq = self.req.pipfile_part.popitem() base_dict = { - k: v - for k, v in self.req.pipfile_part[name].items() + k: v for k, v in dict_from_subreq.items() if k not in ["req", "link", "_setup_info"] } base_dict.update(req_dict) diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 51bf9044..b0b55d47 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -33,7 +33,8 @@ get_pyproject, init_requirement, split_vcs_method_from_uri, - strip_extras_markers_from_requirement + strip_extras_markers_from_requirement, + get_default_pyproject_backend ) try: @@ -431,7 +432,7 @@ class SetupInfo(object): version = attr.ib(default=None, cmp=True) # type: Text _requirements = attr.ib(type=frozenset, factory=frozenset, cmp=True, hash=True) build_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) - build_backend = attr.ib(default="setuptools.build_meta:__legacy__", cmp=True) # type: Text + build_backend = attr.ib(cmp=True) # type: Text setup_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None, cmp=True) _extras_requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) @@ -442,6 +443,10 @@ class SetupInfo(object): extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict, cmp=False, hash=False) metadata = attr.ib(default=None) # type: Optional[Tuple[Text]] + @build_backend.default + def get_build_backend(self): + return get_default_pyproject_backend() + @property def requires(self): # type: () -> Dict[Text, RequirementType] @@ -764,7 +769,7 @@ def run_pyproject(self): if backend: self.build_backend = backend else: - self.build_backend = "setuptools.build_meta:__legacy__" + self.build_backend = get_default_pyproject_backend() if requires: self.build_requires = tuple(set(requires) | set(self.build_requires)) else: diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index eccf5e66..6ad4972d 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -18,8 +18,10 @@ from first import first from packaging.markers import InvalidMarker, Marker, Op, Value, Variable from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet +from packaging.version import parse as parse_version from six.moves.urllib import parse as urllib_parse from urllib3 import util as urllib3_util +from vistir.compat import lru_cache from vistir.misc import dedup from vistir.path import is_valid_url @@ -321,6 +323,26 @@ def _strip_extras_markers(marker): return marker +@lru_cache() +def get_setuptools_version(): + # type: () -> Optional[Text] + import pkg_resources + setuptools_dist = pkg_resources.get_distribution( + pkg_resources.Requirement("setuptools") + ) + return getattr(setuptools_dist, "version", None) + + +def get_default_pyproject_backend(): + # type: () -> Text + st_version = get_setuptools_version() + if st_version is not None: + parsed_st_version = parse_version(st_version) + if parsed_st_version >= parse_version("40.8.0"): + return "setuptools.build_meta:__legacy__" + return "setuptools.build_meta" + + def get_pyproject(path): # type: (Union[Text, Path]) -> Tuple[List[Text], Text] """ @@ -344,8 +366,8 @@ def get_pyproject(path): if not pp_toml.exists(): if not setup_py.exists(): return None - requires = ["setuptools>=40.6", "wheel"] - backend = "setuptools.build_meta:__legacy__" + requires = ["setuptools>=40.8", "wheel"] + backend = get_default_pyproject_backend() else: pyproject_data = {} with io.open(pp_toml.as_posix(), encoding="utf-8") as fh: @@ -353,19 +375,19 @@ def get_pyproject(path): build_system = pyproject_data.get("build-system", None) if build_system is None: if setup_py.exists(): - requires = ["setuptools>=40.6", "wheel"] - backend = "setuptools.build_meta:__legacy__" + requires = ["setuptools>=40.8", "wheel"] + backend = get_default_pyproject_backend() else: - requires = ["setuptools>=40.6", "wheel"] - backend = "setuptools.build_meta" + requires = ["setuptools>=40.8", "wheel"] + backend = get_default_pyproject_backend() build_system = { "requires": requires, "build-backend": backend } pyproject_data["build_system"] = build_system else: - requires = build_system.get("requires", ["setuptools>=40.6", "wheel"]) - backend = build_system.get("build-backend", "setuptools.build_meta:__legacy__") + requires = build_system.get("requires", ["setuptools>=40.8", "wheel"]) + backend = build_system.get("build-backend", get_default_pyproject_backend()) return (requires, backend) From 1f373d328a2afdfa2ca93b712fbcfd508bd9921e Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sun, 24 Feb 2019 20:20:09 -0500 Subject: [PATCH 22/35] Fix test failures Signed-off-by: Dan Ryan --- src/requirementslib/models/setup_info.py | 1 + tests/unit/test_setup_info.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index b0b55d47..dc755c9c 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -652,6 +652,7 @@ def build_wheel(self): dist_type="wheel" ) + # noinspection PyPackageRequirements def build_sdist(self): # type: () -> Text if not self.pyproject.exists(): diff --git a/tests/unit/test_setup_info.py b/tests/unit/test_setup_info.py index 5468585b..309a6e14 100644 --- a/tests/unit/test_setup_info.py +++ b/tests/unit/test_setup_info.py @@ -54,7 +54,7 @@ def find_metadata(path): if not pth: pth = next(iter( pth for pth in os.listdir(path) - if any(pth.endswith(md_ending) for md_ending in [".egg-info", ".dist-info"]) + if any(pth.endswith(md_ending) for md_ending in [".egg-info", ".dist-info", ".whl"]) ), None) return pth @@ -62,7 +62,10 @@ def find_metadata(path): assert not find_metadata(os.path.join(base_dir, "reqlib-metadata")) assert not find_metadata(os.path.join(base_dir, "src", "reqlib-metadata")) assert r.req.setup_info and os.path.isdir(r.req.setup_info.egg_base) - assert find_metadata(r.req.setup_info.egg_base) or find_metadata(r.req.setup_info.extra_kwargs["build_dir"]) + assert ( + find_metadata(r.req.setup_info.egg_base) or + find_metadata(r.req.setup_info.extra_kwargs["build_dir"]) + ) def test_without_extras(pathlib_tmpdir): From 824fb9e1d81689011705f415f74633b5ca772286 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sun, 24 Feb 2019 20:25:53 -0500 Subject: [PATCH 23/35] Drop unused cached_property import Signed-off-by: Dan Ryan --- src/requirementslib/models/setup_info.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 7d34042e..2afb7ae8 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -15,7 +15,6 @@ import pep517.wrappers import six from appdirs import user_cache_dir -from cached_property import cached_property from distlib.wheel import Wheel from packaging.markers import Marker from six.moves import configparser From 8b05caed4fb410f7d699bccf85dd72b56304c525 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sun, 24 Feb 2019 22:20:45 -0500 Subject: [PATCH 24/35] Fix code quality check issues Signed-off-by: Dan Ryan --- src/requirementslib/models/pipfile.py | 8 +++++--- src/requirementslib/models/requirements.py | 10 +++++----- src/requirementslib/models/setup_info.py | 8 ++++---- src/requirementslib/models/utils.py | 8 +++++--- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/requirementslib/models/pipfile.py b/src/requirementslib/models/pipfile.py index ba7eeed5..22eee048 100644 --- a/src/requirementslib/models/pipfile.py +++ b/src/requirementslib/models/pipfile.py @@ -22,7 +22,7 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Union, Any, Dict, Iterable, Sequence, Mapping, List, NoReturn, Text + from typing import Union, Any, Dict, Iterable, Mapping, List, Text package_type = Dict[Text, Dict[Text, Union[List[Text], Text]]] source_type = Dict[Text, Union[Text, bool]] sources_type = Iterable[source_type] @@ -264,7 +264,8 @@ def read_projectfile(cls, path): @classmethod def load_projectfile(cls, path, create=False): # type: (Text, bool) -> ProjectFile - """Given a path, load or create the necessary pipfile. + """ + Given a path, load or create the necessary pipfile. :param Text path: Path to the project root or pipfile :param bool create: Whether to create the pipfile if not found, defaults to True @@ -289,7 +290,8 @@ def load_projectfile(cls, path, create=False): @classmethod def load(cls, path, create=False): # type: (Text, bool) -> Pipfile - """Given a path, load or create the necessary pipfile. + """ + Given a path, load or create the necessary pipfile. :param Text path: Path to the project root or pipfile :param bool create: Whether to create the pipfile if not found, defaults to True diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 18b83195..65c9fa7a 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -77,7 +77,7 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Generator, Set, Text + from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Set, Text from pip_shims.shims import Link, InstallRequirement RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) from six.moves.urllib.parse import SplitResult @@ -320,8 +320,8 @@ def specifiers(self): @specifiers.setter def specifiers(self, specifiers): # type: (Union[Text, SpecifierSet]) -> None - if type(specifiers) is not SpecifierSet: - if type(specifiers) in six.string_types: + if not isinstance(specifiers, SpecifierSet): + if isinstance(specifiers, six.string_types): specifiers = SpecifierSet(specifiers) else: raise TypeError("Must pass a string or a SpecifierSet") @@ -739,7 +739,7 @@ def vcsrepo(self, repo): wheel_kwargs = self.wheel_kwargs.copy() wheel_kwargs["src_dir"] = repo.checkout_directory ireq.source_dir = wheel_kwargs["src_dir"] - build_dir = ireq.build_location(wheel_kwargs["build_dir"]) + ireq.build_location(wheel_kwargs["build_dir"]) ireq._temp_build_dir.path = wheel_kwargs["build_dir"] with temp_path(): sys.path = [repo.checkout_directory, "", ".", get_python_lib(plat_specific=0)] @@ -1789,7 +1789,7 @@ def from_pipfile(cls, name, pipfile): line = "{0}{1}".format(line, extras_to_string(extras)) if "subdirectory" in pipfile: arg_dict["subdirectory"] = pipfile["subdirectory"] - line = "{0}&subdirectory={1}".format(pipfile["subdirectory"]) + line = "{0}&subdirectory={1}".format(line, pipfile["subdirectory"]) if pipfile.get("editable", False): line = "-e {0}".format(line) arg_dict["line"] = line diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index 2afb7ae8..e599f6f7 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -302,8 +302,8 @@ def get_metadata_from_wheel(wheel_path): name = metadata.name version = metadata.version requires = [] - extras_keys = getattr(metadata, "extras", None) - extras = {} + extras_keys = getattr(metadata, "extras", []) + extras = {k: [] for k in extras_keys} for req in getattr(metadata, "run_requires", []): parsed_req = init_requirement(req) parsed_marker = parsed_req.marker @@ -839,7 +839,7 @@ def from_ireq(cls, ireq, subdir=None, finder=None): from .dependencies import get_finder finder = get_finder() - vcs_method, uri = split_vcs_method_from_uri(unquote(ireq.link.url_without_fragment)) + _, uri = split_vcs_method_from_uri(unquote(ireq.link.url_without_fragment)) parsed = urlparse(uri) if "file" in parsed.scheme: url_path = parsed.path @@ -870,7 +870,7 @@ def from_ireq(cls, ireq, subdir=None, finder=None): "The file URL points to a directory not installable: {}" .format(ireq.link) ) - build_dir = ireq.build_location(kwargs["build_dir"]) + ireq.build_location(kwargs["build_dir"]) src_dir = ireq.ensure_has_source_dir(kwargs["src_dir"]) ireq._temp_build_dir.path = kwargs["build_dir"] diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index 6ad4972d..387997f3 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -295,9 +295,11 @@ def strip_extras_markers_from_requirement(req): raise TypeError("Must pass in a valid requirement, received {0!r}".format(req)) if getattr(req, "marker", None) is not None: marker = req.marker # type: TMarker - req.marker._markers = _strip_extras_markers(req.marker._markers) - if not req.marker._markers: + marker._markers = _strip_extras_markers(marker._markers) + if not marker._markers: req.marker = None + else: + req.marker = marker return req @@ -354,9 +356,9 @@ def get_pyproject(path): :rtype: Tuple[List[Text], Text] """ - from vistir.compat import Path if not path: return + from vistir.compat import Path if not isinstance(path, Path): path = Path(path) if not path.is_dir(): From df44ced33df61d73f92bfbc191cefca7da21bae6 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sun, 24 Feb 2019 22:37:25 -0500 Subject: [PATCH 25/35] Add typed-ast to dependencies Signed-off-by: Dan Ryan --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 768f9f33..fa2ba968 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,6 +87,7 @@ typing = monkeytype;python_version>="3.4" pytype;python_version>="3.4" pyannotate + typed-ast;python_version>="3.4" [bdist_wheel] From 5a4b9cf45eddeb65f1a7c2d0987b9cf12d1eb7d7 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Mon, 25 Feb 2019 01:01:16 -0500 Subject: [PATCH 26/35] Update lockfile Signed-off-by: Dan Ryan --- Pipfile.lock | 381 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 221 insertions(+), 160 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 3809326c..e48b05d1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -52,10 +52,10 @@ }, "atomicwrites": { "hashes": [ - "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", - "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" ], - "version": "==1.2.1" + "version": "==1.3.0" }, "attrs": { "hashes": [ @@ -110,6 +110,13 @@ ], "version": "==3.1.0" }, + "cached-property": { + "hashes": [ + "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", + "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" + ], + "version": "==1.5.1" + }, "cerberus": { "hashes": [ "sha256:f5c2e048fb15ecb3c088d192164316093fcfa602a74b3386eefb2983aa7e800a" @@ -125,40 +132,36 @@ }, "cffi": { "hashes": [ - "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", - "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", - "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", - "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", - "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30", - "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", - "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", - "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b", - "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", - "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e", - "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", - "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", - "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", - "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", - "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", - "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", - "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", - "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", - "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5", - "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", - "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", - "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", - "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", - "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", - "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2", - "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", - "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", - "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", - "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", - "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", - "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", - "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" - ], - "version": "==1.11.5" + "sha256:0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6", + "sha256:27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709", + "sha256:2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a", + "sha256:3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282", + "sha256:358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556", + "sha256:37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134", + "sha256:39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d", + "sha256:42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da", + "sha256:5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1", + "sha256:5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509", + "sha256:5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1", + "sha256:7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3", + "sha256:7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96", + "sha256:7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2", + "sha256:8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28", + "sha256:9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c", + "sha256:9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033", + "sha256:a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d", + "sha256:aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea", + "sha256:bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0", + "sha256:bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469", + "sha256:bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071", + "sha256:c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719", + "sha256:d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a", + "sha256:df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6", + "sha256:f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a", + "sha256:f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf", + "sha256:f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729" + ], + "version": "==1.12.1" }, "chardet": { "hashes": [ @@ -225,13 +228,16 @@ "hashes": [ "sha256:029c69deaeeeae1b15bc6c59f0ffa28aa8473721c614a23f2c2976dec245cd12", "sha256:02abbbebc6e9d5abe13cd28b5e963dedb6ffb51c146c916d17b18f141acd9947", + "sha256:0384e4479aeb780bfb811eb54c3deb37dbc5e197cd19ec1190cb4b33b254f661", "sha256:1bbfe5b82a3921d285e999c6d256c1e16b31c554c29da62d326f86c173d30337", + "sha256:20bbeef0d08e43ef44e10d5863225e178fe100d5778c616260286202bad9d2b4", "sha256:210c02f923df33a8d0e461c86fdcbbb17228ff4f6d92609fc06370a98d283c2d", "sha256:2d0807ba935f540d20b49d5bf1c0237b90ce81e133402feda906e540003f2f7a", "sha256:35d7a013874a7c927ce997350d314144ffc5465faf787bb4e46e6c4f381ef562", "sha256:3636f9d0dcb01aed4180ef2e57a4e34bb4cac3ecd203c2a23db8526d86ab2fb4", "sha256:42f4be770af2455a75e4640f033a82c62f3fb0d7a074123266e143269d7010ef", "sha256:48440b25ba6cda72d4c638f3a9efa827b5b87b489c96ab5f4ff597d976413156", + "sha256:494fc6f09b776668cb0d69df5caefb9b90867bd280eb1bd004a63c79fbb09e71", "sha256:4dac8dfd1acf6a3ac657475dfdc66c621f291b1b7422a939cc33c13ac5356473", "sha256:4e8474771c69c2991d5eab65764289a7dd450bbea050bc0ebb42b678d8222b42", "sha256:551f10ddfeff56a1325e5a34eff304c5892aa981fd810babb98bfee77ee2fb17", @@ -240,14 +246,21 @@ "sha256:633151f8d1ad9467b9f7e90854a7f46ed8f2919e8bc7d98d737833e8938fc081", "sha256:772207b9e2d5bf3f9d283b88915723e4e92d9a62c83f44ec92b9bd0cd685541b", "sha256:7d5e02f647cd727afc2659ec14d4d1cc0508c47e6cfb07aea33d7aa9ca94d288", + "sha256:8edc25c1449bdf31acfe183e579bb9c75cec59b55220ccefb6a4f960807ef1d0", + "sha256:96d895fba9ed55286368bd4626b8dcbf19b9a529a88e5a6b5c22e0b08c95852a", + "sha256:a77589fec63dc7fa6469d8cd122cc285ec034be43d8a2ba600020ddb14277514", "sha256:a9798a4111abb0f94584000ba2a2c74841f2cfe5f9254709756367aabbae0541", + "sha256:b33a8f3d6d8946ea1db4ec228606ebc45cf880a7b1d1a26fe8790af677c12b8b", "sha256:b38ea741ab9e35bfa7015c93c93bbd6a1623428f97a67083fc8ebd366238b91f", "sha256:b6a5478c904236543c0347db8a05fac6fc0bd574c870e7970faa88e1d9890044", "sha256:c6248bfc1de36a3844685a2e10ba17c18119ba6252547f921062a323fb31bff1", + "sha256:c6c6f84282d3f8953072295ce5cb96cdc56c91f164ef451a5c03be8abb84ad56", "sha256:c705ab445936457359b1424ef25ccc0098b0491b26064677c39f1d14a539f056", "sha256:d95a363d663ceee647291131dbd213af258df24f41350246842481ec3709bd33", "sha256:e27265eb80cdc5dab55a40ef6f890e04ecc618649ad3da5265f128b141f93f78", + "sha256:eb62a45b448258bd8b9faa2d12dc2b942259715d7e0d85ebbb3d737be83091d7", "sha256:ebc276c9cb5d917bd2ae959f84ffc279acafa9c9b50b0fa436ebb70bbe2166ea", + "sha256:f05a38b77b6c62cff204b0874034d76709769b53a8a7fc5886e02fc4d099d540", "sha256:f4d229866d030863d0fe3bf297d6d11e6133ca15bbb41ed2534a8b9a3d6bd061", "sha256:f95675bd88b51474d4fe5165f3266f419ce754ffadfb97f10323931fa9ac95e5", "sha256:f95bc54fb6d61b9f9ff09c4ae8ff6a3f5edc937cda3ca36fc937302a7c152bf1", @@ -257,16 +270,16 @@ }, "cursor": { "hashes": [ - "sha256:8ee9fe5b925e1001f6ae6c017e93682583d2b4d1ef7130a26cfcdf1651c0032c" + "sha256:33f279a17789c04efd27a92501a0dad62bb011f8a4cdff93867c798d26508940" ], - "version": "==1.2.0" + "version": "==1.3.4" }, "decorator": { "hashes": [ - "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", - "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + "sha256:33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e", + "sha256:cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b" ], - "version": "==4.3.0" + "version": "==4.3.2" }, "distlib": { "hashes": [ @@ -288,6 +301,13 @@ ], "version": "==0.14" }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, "enum34": { "hashes": [ "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", @@ -310,7 +330,6 @@ "sha256:4577a5e2c7c6c93270d95bcdf1158c7beb6841af4e400a93caf2ec6548f28a4c", "sha256:fae8d6ed10f87ac30840d9c8c68fbe72f36bf3641e2b396c7543f610464bef09" ], - "markers": "python_version >= '3.4'", "version": "==1.4.1" }, "first": { @@ -322,10 +341,10 @@ }, "flake8": { "hashes": [ - "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", - "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" + "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048", + "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383" ], - "version": "==3.6.0" + "version": "==3.7.6" }, "funcsigs": { "hashes": [ @@ -368,7 +387,6 @@ "hashes": [ "sha256:ab3a0bf77a326de577e3c7f643ec304f83fed93cb1056638560d832413d6e736" ], - "markers": "python_version >= '3.4'", "version": "==0.5" }, "incremental": { @@ -388,10 +406,10 @@ }, "jedi": { "hashes": [ - "sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd", - "sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191" + "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b", + "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c" ], - "version": "==0.13.2" + "version": "==0.13.3" }, "jinja2": { "hashes": [ @@ -434,36 +452,36 @@ }, "markupsafe": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" - ], - "version": "==1.1.0" + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" }, "mccabe": { "hashes": [ @@ -477,24 +495,23 @@ "sha256:baeeee422c17202038ccf17ca73eb97eddb65a4178a215c1ff212cfb7373eb65", "sha256:ecee4162a153c8a0d2151dfc66f06ebb82e4582b0d46281798d908888bb0c9b9" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.4'", "version": "==19.1.1" }, "more-itertools": { "hashes": [ - "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", - "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", - "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" ], - "version": "==5.0.0" + "version": "==6.0.0" }, "mypy": { "hashes": [ - "sha256:986a7f97808a865405c5fd98fae5ebfa963c31520a56c783df159e9a81e41b3e", - "sha256:cc5df73cc11d35655a8c364f45d07b13c8db82c000def4bd7721be13356533b4" + "sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7", + "sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d" ], "markers": "python_version >= '3.4'", - "version": "==0.660" + "version": "==0.670" }, "mypy-extensions": { "hashes": [ @@ -507,13 +524,13 @@ "hashes": [ "sha256:ce16c7917b292b78bf1c6f90b2d87c972fa95f11543a679cc1640ade004bd103" ], + "markers": "python_version >= '3.4'", "version": "==0.1.15" }, "networkx": { "hashes": [ "sha256:45e56f7ab6fe81652fb4bc9f44faddb0e9025f469f602df14e3b2551c2ea5c8b" ], - "markers": "python_version >= '3.4'", "version": "==2.2" }, "ninja": { @@ -547,7 +564,6 @@ "sha256:ee540cf952adefe9b8452bb83a928c52de1a05fd32f4bfd8a66c7426defeeedc", "sha256:ee8bd1b78fbef6b6ca827caa0bb661d2ed7f2936d261b21c656516d3ad78c538" ], - "markers": "python_version >= '3.4'", "version": "==1.8.2.post2" }, "packaging": { @@ -559,10 +575,10 @@ }, "parso": { "hashes": [ - "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", - "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + "sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9", + "sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db" ], - "version": "==0.3.1" + "version": "==0.3.4" }, "parver": { "hashes": [ @@ -620,18 +636,18 @@ }, "pluggy": { "hashes": [ - "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", - "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" ], - "version": "==0.8.1" + "version": "==0.9.0" }, "prompt-toolkit": { "hashes": [ - "sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34", - "sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9", - "sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39" + "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", + "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", + "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" ], - "version": "==2.0.7" + "version": "==2.0.9" }, "ptpython": { "hashes": [ @@ -644,10 +660,10 @@ }, "py": { "hashes": [ - "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", - "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" ], - "version": "==1.7.0" + "version": "==1.8.0" }, "pyannotate": { "hashes": [ @@ -658,23 +674,24 @@ }, "pycodestyle": { "hashes": [ - "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", - "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" ], - "version": "==2.4.0" + "version": "==2.5.0" }, "pycparser": { "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3", + "sha256:c40ce4f562872546bf168eab2f3b34a2200e169099182621bf3429cdec8777b4" ], "version": "==2.19" }, "pyflakes": { "hashes": [ - "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", - "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" + "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", + "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd" ], - "version": "==2.0.0" + "version": "==2.1.0" }, "pygments": { "hashes": [ @@ -714,10 +731,10 @@ }, "pytest-forked": { "hashes": [ - "sha256:260d03fbd38d5ce41a657759e8d19bc7c8cfa6d0dcfa36c0bc9742d33bc30742", - "sha256:8d05c2e6f33cd4422571b2b1bb309720c398b0549cff499e3e4cde661875ab54" + "sha256:5fe33fbd07d7b1302c95310803a5e5726a4ff7f19d5a542b7ce57c76fed8135f", + "sha256:d352aaced2ebd54d42a65825722cb433004b4446ab5d2044851d9cc7a00c9e38" ], - "version": "==1.0.1" + "version": "==1.0.2" }, "pytest-sugar": { "hashes": [ @@ -736,10 +753,10 @@ }, "pytest-xdist": { "hashes": [ - "sha256:107e9db0ee30ead02ca93e7d6d4846675f1b2142234f0eb1cd4d76739cd9ae6f", - "sha256:5795f665e112520fa5beab736ad957e7f36ce7d44210f4004be9d99f86529d97" + "sha256:4a201bb3ee60f5dd6bb40c5209d4e491cecc4d5bafd656cfb10f86178786e568", + "sha256:d03d1ff1b008458ed04fa73e642d840ac69b4107c168e06b71037c62d7813dd4" ], - "version": "==1.26.0" + "version": "==1.26.1" }, "pytoml": { "hashes": [ @@ -749,10 +766,10 @@ }, "pytype": { "hashes": [ - "sha256:35d015c365a098e28f975f4e323d0fb5490285ffc85e3b98ddf902cc96a06ea9" + "sha256:7213ed98a06c36cdf76bf22fd4db4806f6a76704bf9126521e43590ee1fc87e4" ], "markers": "python_version >= '3.4'", - "version": "==2019.1.18" + "version": "==2019.2.13" }, "pytz": { "hashes": [ @@ -763,11 +780,15 @@ }, "pyyaml": { "hashes": [ + "sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110", "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", + "sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7", "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", - "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" + "sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8", + "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b", + "sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d" ], "version": "==4.2b4" }, @@ -790,10 +811,10 @@ }, "requests-toolbelt": { "hashes": [ - "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", - "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" ], - "version": "==0.8.0" + "version": "==0.9.1" }, "requirementslib": { "editable": true, @@ -809,7 +830,6 @@ "sha256:33cfb36601bfeb355924731d8db78fa82f3f12eb37e87236e9179d81aba97740", "sha256:b64b767befbe6f5fd918603ab7d6bbff07fc4c431bae2f471e195677a0c9b327" ], - "markers": "python_version >= '3.4'", "version": "==17.12.0" }, "rope": { @@ -866,6 +886,48 @@ "index": "pypi", "version": "==0.4.2" }, + "sphinxcontrib-applehelp": { + "hashes": [ + "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", + "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-devhelp": { + "hashes": [ + "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", + "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-htmlhelp": { + "hashes": [ + "sha256:0d691ca8edf5995fbacfe69b191914256071a94cbad03c3688dca47385c9206c", + "sha256:e31c8271f5a8f04b620a500c0442a7d5cfc1a732fa5c10ec363f90fe72af0cb8" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-jsmath": { + "hashes": [ + "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", + "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-qthelp": { + "hashes": [ + "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", + "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" + ], + "version": "==1.0.2" + }, + "sphinxcontrib-serializinghtml": { + "hashes": [ + "sha256:01d9b2617d7e8ddf7a00cae091f08f9fa4db587cc160b493141ee56710810932", + "sha256:392187ac558863b8aff0d76dc78e0731fed58f3b06e2b00e22995dcdb630f213" + ], + "version": "==1.1.1" + }, "sphinxcontrib-websupport": { "hashes": [ "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", @@ -882,7 +944,8 @@ "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3" ], "version": "==0.10.0" }, @@ -903,44 +966,42 @@ }, "tqdm": { "hashes": [ - "sha256:b856be5cb6cfaee3b2733655c7c5bbc7751291bb5d1a4f54f020af4727570b3e", - "sha256:c9b9b5eeba13994a4c266aae7eef7aeeb0ba2973e431027e942b4faea139ef49" + "sha256:d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021", + "sha256:e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05" ], - "version": "==4.29.1" + "version": "==4.31.1" }, "twine": { "hashes": [ - "sha256:7d89bc6acafb31d124e6e5b295ef26ac77030bf098960c2a4c4e058335827c5c", - "sha256:fad6f1251195f7ddd1460cb76d6ea106c93adb4e56c41e0da79658e56e547d2c" + "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", + "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc" ], - "version": "==1.12.1" + "version": "==1.13.0" }, "typed-ast": { "hashes": [ - "sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe", - "sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c", - "sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2", - "sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a", - "sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7", - "sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827", - "sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33", - "sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9", - "sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032", - "sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9", - "sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2", - "sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2", - "sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062", - "sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15", - "sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357", - "sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a", - "sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824", - "sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442", - "sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1", - "sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2", - "sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6" - ], - "markers": "python_version >= '3.3' and python_version <= '3.6'", - "version": "==1.2.0" + "sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23", + "sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15", + "sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3", + "sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d", + "sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6", + "sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60", + "sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773", + "sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424", + "sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287", + "sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99", + "sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23", + "sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8", + "sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699", + "sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1", + "sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463", + "sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6", + "sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0", + "sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0", + "sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6" + ], + "markers": "python_version >= '3.4'", + "version": "==1.3.1" }, "typing": { "hashes": [ @@ -998,17 +1059,17 @@ }, "wheel": { "hashes": [ - "sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6", - "sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44" + "sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d", + "sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668" ], - "version": "==0.32.3" + "version": "==0.33.1" }, "yaspin": { "hashes": [ - "sha256:36fdccc5e0637b5baa8892fe2c3d927782df7d504e9020f40eb2c1502518aa5a", - "sha256:8e52bf8079a48e2a53f3dfeec9e04addb900c101d1591c85df69cf677d3237e7" + "sha256:441f8a6761e347652d04614899fd0a9cfda7439e2d5682e664bd31230c656176", + "sha256:d3ebcf8162e0ef8bb5484b8751d5b6d2fbf0720112c81f64614c308576a03b1d" ], - "version": "==0.14.0" + "version": "==0.14.1" } } } From 6a3e4332740c3820b153155462c3e80ee9ce9205 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Mon, 25 Feb 2019 01:50:25 -0500 Subject: [PATCH 27/35] Update readme Signed-off-by: Dan Ryan --- README.rst | 20 -------------------- src/requirementslib/models/requirements.py | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/README.rst b/README.rst index b0379469..14e7d131 100644 --- a/README.rst +++ b/README.rst @@ -200,23 +200,3 @@ requirement itself via the property ``requirement.req.dependencies``: * `Pip `_ * `Pipenv `_ * `Pipfile`_ -======= -Copyright 2016 Kenneth Reitz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 65c9fa7a..5d3f6c5b 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -2602,7 +2602,7 @@ def from_line(cls, line): r.req.extras = args["extras"] if parsed_line.hashes: args["hashes"] = tuple(parsed_line.hashes) # type: ignore - cls_inst = cls(**args) + cls_inst = cls(**args) # type: ignore return cls_inst @classmethod From d48d59ba6a115a6f205704bd8dfa6432aa718111 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 26 Feb 2019 11:52:44 -0500 Subject: [PATCH 28/35] Update lockfile with correct markers Signed-off-by: Dan Ryan --- Pipfile.lock | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index e48b05d1..6e4453f7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -219,10 +219,11 @@ }, "configparser": { "hashes": [ - "sha256:5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a" + "sha256:27594cf4fc279f321974061ac69164aaebd2749af962ac8686b20503ac0bcf2d", + "sha256:9d51fe0a382f05b6b117c5e601fc219fede4a8c71703324af3f7d883aef476a3" ], "markers": "python_version < '3.2'", - "version": "==3.5.0" + "version": "==3.7.3" }, "coverage": { "hashes": [ @@ -341,10 +342,10 @@ }, "flake8": { "hashes": [ - "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048", - "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383" + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" ], - "version": "==3.7.6" + "version": "==3.7.7" }, "funcsigs": { "hashes": [ @@ -682,7 +683,7 @@ "pycparser": { "hashes": [ "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3", - "sha256:c40ce4f562872546bf168eab2f3b34a2200e169099182621bf3429cdec8777b4" + "sha256:b56a182c2fa7218e1395c30348d1685b82813edd65901a973b7d73eabb225d9d" ], "version": "==2.19" }, @@ -830,6 +831,7 @@ "sha256:33cfb36601bfeb355924731d8db78fa82f3f12eb37e87236e9179d81aba97740", "sha256:b64b767befbe6f5fd918603ab7d6bbff07fc4c431bae2f471e195677a0c9b327" ], + "markers": "python_version > '3.5'", "version": "==17.12.0" }, "rope": { @@ -884,6 +886,7 @@ "sha256:d0f6bc70f98961145c5b0e26a992829363a197321ba571b31b24ea91879e0c96" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==0.4.2" }, "sphinxcontrib-applehelp": { @@ -891,6 +894,7 @@ "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-devhelp": { @@ -898,6 +902,7 @@ "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-htmlhelp": { @@ -905,6 +910,7 @@ "sha256:0d691ca8edf5995fbacfe69b191914256071a94cbad03c3688dca47385c9206c", "sha256:e31c8271f5a8f04b620a500c0442a7d5cfc1a732fa5c10ec363f90fe72af0cb8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-jsmath": { @@ -912,6 +918,7 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -919,6 +926,7 @@ "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-serializinghtml": { @@ -926,6 +934,7 @@ "sha256:01d9b2617d7e8ddf7a00cae091f08f9fa4db587cc160b493141ee56710810932", "sha256:392187ac558863b8aff0d76dc78e0731fed58f3b06e2b00e22995dcdb630f213" ], + "markers": "python_version >= '3.5'", "version": "==1.1.1" }, "sphinxcontrib-websupport": { From 9787d175ab9b8a0414bbc074cb88372a35b7c1fc Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 26 Feb 2019 12:15:31 -0500 Subject: [PATCH 29/35] Update lockfile Signed-off-by: Dan Ryan --- Pipfile.lock | 71 ++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 6e4453f7..18b59370 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -132,36 +132,36 @@ }, "cffi": { "hashes": [ - "sha256:0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6", - "sha256:27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709", - "sha256:2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a", - "sha256:3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282", - "sha256:358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556", - "sha256:37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134", - "sha256:39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d", - "sha256:42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da", - "sha256:5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1", - "sha256:5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509", - "sha256:5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1", - "sha256:7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3", - "sha256:7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96", - "sha256:7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2", - "sha256:8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28", - "sha256:9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c", - "sha256:9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033", - "sha256:a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d", - "sha256:aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea", - "sha256:bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0", - "sha256:bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469", - "sha256:bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071", - "sha256:c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719", - "sha256:d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a", - "sha256:df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6", - "sha256:f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a", - "sha256:f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf", - "sha256:f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729" - ], - "version": "==1.12.1" + "sha256:00b97afa72c233495560a0793cdc86c2571721b4271c0667addc83c417f3d90f", + "sha256:0ba1b0c90f2124459f6966a10c03794082a2f3985cd699d7d63c4a8dae113e11", + "sha256:0bffb69da295a4fc3349f2ec7cbe16b8ba057b0a593a92cbe8396e535244ee9d", + "sha256:21469a2b1082088d11ccd79dd84157ba42d940064abbfa59cf5f024c19cf4891", + "sha256:2e4812f7fa984bf1ab253a40f1f4391b604f7fc424a3e21f7de542a7f8f7aedf", + "sha256:2eac2cdd07b9049dd4e68449b90d3ef1adc7c759463af5beb53a84f1db62e36c", + "sha256:2f9089979d7456c74d21303c7851f158833d48fb265876923edcb2d0194104ed", + "sha256:3dd13feff00bddb0bd2d650cdb7338f815c1789a91a6f68fdc00e5c5ed40329b", + "sha256:4065c32b52f4b142f417af6f33a5024edc1336aa845b9d5a8d86071f6fcaac5a", + "sha256:51a4ba1256e9003a3acf508e3b4f4661bebd015b8180cc31849da222426ef585", + "sha256:59888faac06403767c0cf8cfb3f4a777b2939b1fbd9f729299b5384f097f05ea", + "sha256:59c87886640574d8b14910840327f5cd15954e26ed0bbd4e7cef95fa5aef218f", + "sha256:610fc7d6db6c56a244c2701575f6851461753c60f73f2de89c79bbf1cc807f33", + "sha256:70aeadeecb281ea901bf4230c6222af0248c41044d6f57401a614ea59d96d145", + "sha256:71e1296d5e66c59cd2c0f2d72dc476d42afe02aeddc833d8e05630a0551dad7a", + "sha256:8fc7a49b440ea752cfdf1d51a586fd08d395ff7a5d555dc69e84b1939f7ddee3", + "sha256:9b5c2afd2d6e3771d516045a6cfa11a8da9a60e3d128746a7fe9ab36dfe7221f", + "sha256:9c759051ebcb244d9d55ee791259ddd158188d15adee3c152502d3b69005e6bd", + "sha256:b4d1011fec5ec12aa7cc10c05a2f2f12dfa0adfe958e56ae38dc140614035804", + "sha256:b4f1d6332339ecc61275bebd1f7b674098a66fea11a00c84d1c58851e618dc0d", + "sha256:c030cda3dc8e62b814831faa4eb93dd9a46498af8cd1d5c178c2de856972fd92", + "sha256:c2e1f2012e56d61390c0e668c20c4fb0ae667c44d6f6a2eeea5d7148dcd3df9f", + "sha256:c37c77d6562074452120fc6c02ad86ec928f5710fbc435a181d69334b4de1d84", + "sha256:c8149780c60f8fd02752d0429246088c6c04e234b895c4a42e1ea9b4de8d27fb", + "sha256:cbeeef1dc3c4299bd746b774f019de9e4672f7cc666c777cd5b409f0b746dac7", + "sha256:e113878a446c6228669144ae8a56e268c91b7f1fafae927adc4879d9849e0ea7", + "sha256:e21162bf941b85c0cda08224dade5def9360f53b09f9f259adb85fc7dd0e7b35", + "sha256:fb6934ef4744becbda3143d30c6604718871495a5e36c408431bf33d9c146889" + ], + "version": "==1.12.2" }, "chardet": { "hashes": [ @@ -316,7 +316,7 @@ "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" ], - "markers": "python_version >= '2.7' and python_version < '2.8'", + "markers": "python_version < '3.4'", "version": "==1.1.6" }, "execnet": { @@ -360,7 +360,7 @@ "sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0", "sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d" ], - "markers": "python_version >= '2.7' and python_version < '2.8'", + "markers": "python_version < '3.2'", "version": "==3.2.3.post2" }, "future": { @@ -501,10 +501,11 @@ }, "more-itertools": { "hashes": [ - "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", - "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", + "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", + "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" ], - "version": "==6.0.0" + "version": "==5.0.0" }, "mypy": { "hashes": [ From 084ca1643eeae61f5b1ac4a38519eb54f7ca2b3d Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 26 Feb 2019 17:34:29 -0500 Subject: [PATCH 30/35] Fix broken requirement in lockfile and update typechecking task Signed-off-by: Dan Ryan --- Pipfile.lock | 2 +- setup.cfg | 1 + src/requirementslib/models/requirements.py | 38 +++++++++++++++------- tasks/__init__.py | 3 +- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 18b59370..bd3af152 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -496,7 +496,7 @@ "sha256:baeeee422c17202038ccf17ca73eb97eddb65a4178a215c1ff212cfb7373eb65", "sha256:ecee4162a153c8a0d2151dfc66f06ebb82e4582b0d46281798d908888bb0c9b9" ], - "markers": "python_version >= '3.4'", + "markers": "python_version >= '3.6'", "version": "==19.1.1" }, "more-itertools": { diff --git a/setup.cfg b/setup.cfg index fa2ba968..2359af89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -174,3 +174,4 @@ ignore_missing_imports=true follow_imports=skip html_report=mypyhtml python_version=2.7 +show_error_context=true diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 5d3f6c5b..67065f85 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -2522,7 +2522,8 @@ def is_vcs(self): @property def build_backend(self): # type: () -> Optional[Text] - if self.is_vcs or (self.is_file_or_url and self.req.is_local): + if self.is_vcs or (self.is_file_or_url and ( + self.req is not None and self.req.is_local)): setup_info = self.run_requires() build_backend = setup_info.get("build_backend") return build_backend @@ -2548,7 +2549,11 @@ def is_named(self): @property def is_wheel(self): # type: () -> bool - if not self.is_named and self.req.link is not None and self.req.link.is_wheel: + if not self.is_named and ( + self.req is not None and + self.req.link is not None and + self.req.link.is_wheel + ): return True return False @@ -2942,6 +2947,9 @@ def merge_markers(self, markers): def file_req_from_parsed_line(parsed_line): # type: (Line) -> FileRequirement path = parsed_line.relpath if parsed_line.relpath else parsed_line.path + pyproject_requires = () # type: Tuple[Text] + if parsed_line.pyproject_requires is not None: + pyproject_requires = tuple(parsed_line.pyproject_requires) return FileRequirement( setup_path=parsed_line.setup_py, path=path, @@ -2950,7 +2958,7 @@ def file_req_from_parsed_line(parsed_line): uri_scheme=parsed_line.preferred_scheme, link=parsed_line.link, uri=parsed_line.uri, - pyproject_requires=tuple(parsed_line.pyproject_requires) if parsed_line.pyproject_requires else None, + pyproject_requires=pyproject_requires, pyproject_backend=parsed_line.pyproject_backend, pyproject_path=Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, parsed_line=parsed_line, @@ -2964,14 +2972,20 @@ def vcs_req_from_parsed_line(parsed_line): line = "{0}".format(parsed_line.line) if parsed_line.editable: line = "-e {0}".format(line) - link = create_link(build_vcs_uri( - vcs=parsed_line.vcs, - uri=parsed_line.url, - name=parsed_line.name, - ref=parsed_line.ref, - subdirectory=parsed_line.subdirectory, - extras=parsed_line.extras - )) + if parsed_line.url is not None: + link = create_link(build_vcs_uri( + vcs=parsed_line.vcs, + uri=parsed_line.url, + name=parsed_line.name, + ref=parsed_line.ref, + subdirectory=parsed_line.subdirectory, + extras=list(parsed_line.extras) + )) + else: + link = parsed_line.link + pyproject_requires = () # type: Tuple[Text] + if parsed_line.pyproject_requires is not None: + pyproject_requires = tuple(parsed_line.pyproject_requires) return VCSRequirement( setup_path=parsed_line.setup_py, path=parsed_line.path, @@ -2983,7 +2997,7 @@ def vcs_req_from_parsed_line(parsed_line): uri_scheme=parsed_line.preferred_scheme, link=link, uri=parsed_line.uri, - pyproject_requires=tuple(parsed_line.pyproject_requires) if parsed_line.pyproject_requires else None, + pyproject_requires=pyproject_requires, pyproject_backend=parsed_line.pyproject_backend, pyproject_path=Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, parsed_line=parsed_line, diff --git a/tasks/__init__.py b/tasks/__init__.py index 3a219558..b870bc3e 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -51,8 +51,9 @@ def find_version(): def typecheck(ctx): src_dir = ROOT / "src" / PACKAGE_NAME src_dir = src_dir.as_posix() + config_file = ROOT / "setup.cfg" env = {"MYPYPATH": src_dir} - ctx.run(f"mypy {src_dir}", env=env) + ctx.run(f"mypy {src_dir} --config-file={config_file}", env=env) @invoke.task() From a55f1364690a51581972a8da0c4339318310a696 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 27 Feb 2019 03:32:12 -0500 Subject: [PATCH 31/35] Update type hints Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 446 +++++++++++---------- src/requirementslib/models/utils.py | 73 ++-- 2 files changed, 271 insertions(+), 248 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 67065f85..60320bdb 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -77,12 +77,13 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Set, Text + from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Set, AnyStr, Text from pip_shims.shims import Link, InstallRequirement RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) from six.moves.urllib.parse import SplitResult from .vcs import VCSRepository NON_STRING_ITERABLE = Union[List, Set, Tuple] + STRING_TYPE = Union[str, bytes, Text] SPECIFIERS_BY_LENGTH = sorted(list(Specifier._operators.keys()), key=len, reverse=True) @@ -93,41 +94,41 @@ class Line(object): def __init__(self, line, extras=None): - # type: (Text, Optional[NON_STRING_ITERABLE]) -> None + # type: (AnyStr, Optional[NON_STRING_ITERABLE]) -> None self.editable = False # type: bool if line.startswith("-e "): line = line[len("-e "):] self.editable = True - self.extras = () # type: Tuple[Text] + self.extras = () # type: Tuple[STRING_TYPE] if extras is not None: self.extras = tuple(sorted(set(extras))) - self.line = line # type: Text - self.hashes = [] # type: List[Text] - self.markers = None # type: Optional[Text] - self.vcs = None # type: Optional[Text] - self.path = None # type: Optional[Text] - self.relpath = None # type: Optional[Text] - self.uri = None # type: Optional[Text] + self.line = line # type: STRING_TYPE + self.hashes = [] # type: List[STRING_TYPE] + self.markers = None # type: Optional[STRING_TYPE] + self.vcs = None # type: Optional[STRING_TYPE] + self.path = None # type: Optional[STRING_TYPE] + self.relpath = None # type: Optional[STRING_TYPE] + self.uri = None # type: Optional[STRING_TYPE] self._link = None # type: Optional[Link] self.is_local = False # type: bool - self._name = None # type: Optional[Text] - self._specifier = None # type: Optional[Text] + self._name = None # type: Optional[STRING_TYPE] + self._specifier = None # type: Optional[STRING_TYPE] self.parsed_marker = None # type: Optional[Marker] - self.preferred_scheme = None # type: Optional[Text] + self.preferred_scheme = None # type: Optional[STRING_TYPE] self._requirement = None # type: Optional[PackagingRequirement] self.is_direct_url = False # type: bool self._parsed_url = None # type: Optional[urllib_parse.ParseResult] - self._setup_cfg = None # type: Optional[Text] - self._setup_py = None # type: Optional[Text] - self._pyproject_toml = None # type: Optional[Text] - self._pyproject_requires = None # type: Optional[List[Text]] - self._pyproject_backend = None # type: Optional[Text] - self._wheel_kwargs = None # type: Dict[Text, Text] + self._setup_cfg = None # type: Optional[STRING_TYPE] + self._setup_py = None # type: Optional[STRING_TYPE] + self._pyproject_toml = None # type: Optional[STRING_TYPE] + self._pyproject_requires = None # type: Optional[Tuple[STRING_TYPE, ...]] + self._pyproject_backend = None # type: Optional[STRING_TYPE] + self._wheel_kwargs = None # type: Dict[STRING_TYPE, STRING_TYPE] self._vcsrepo = None # type: Optional[VCSRepository] self._setup_info = None # type: Optional[SetupInfo] - self._ref = None # type: Optional[Text] + self._ref = None # type: Optional[STRING_TYPE] self._ireq = None # type: Optional[InstallRequirement] - self._src_root = None # type: Optional[Text] + self._src_root = None # type: Optional[STRING_TYPE] self.dist = None # type: Any super(Line, self).__init__() self.parse() @@ -153,12 +154,12 @@ def __repr__(self): @classmethod def split_hashes(cls, line): - # type: (Text) -> Tuple[Text, List[Text]] + # type: (AnyStr) -> Tuple[AnyStr, List[AnyStr]] if "--hash" not in line: return line, [] split_line = line.split() - line_parts = [] # type: List[Text] - hashes = [] # type: List[Text] + line_parts = [] # type: List[STRING_TYPE] + hashes = [] # type: List[STRING_TYPE] for part in split_line: if part.startswith("--hash"): param, _, value = part.partition("=") @@ -170,7 +171,7 @@ def split_hashes(cls, line): @property def line_with_prefix(self): - # type: () -> Text + # type: () -> STRING_TYPE line = self.line extras_str = extras_to_string(self.extras) if self.is_direct_url: @@ -190,7 +191,7 @@ def line_with_prefix(self): @property def line_for_ireq(self): - # type: () -> Text + # type: () -> STRING_TYPE line = "" if self.is_file or self.is_url and not self.is_vcs: scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" @@ -231,7 +232,7 @@ def line_for_ireq(self): @property def base_path(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if not self.link and not self.path: self.parse_link() if not self.path: @@ -247,28 +248,28 @@ def base_path(self): @property def setup_py(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._setup_py is None: self.populate_setup_paths() return self._setup_py @property def setup_cfg(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._setup_cfg is None: self.populate_setup_paths() return self._setup_cfg @property def pyproject_toml(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._pyproject_toml is None: self.populate_setup_paths() return self._pyproject_toml @property def specifier(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] options = [self._specifier] for req in (self.ireq, self.requirement): if req is not None and getattr(req, "specifier", None): @@ -319,7 +320,7 @@ def specifiers(self): @specifiers.setter def specifiers(self, specifiers): - # type: (Union[Text, SpecifierSet]) -> None + # type: (Union[Text, str, SpecifierSet]) -> None if not isinstance(specifiers, SpecifierSet): if isinstance(specifiers, six.string_types): specifiers = SpecifierSet(specifiers) @@ -335,7 +336,7 @@ def specifiers(self, specifiers): @classmethod def get_requirement_specs(cls, specifierset): - # type: (SpecifierSet) -> List[Tuple[Text, Text]] + # type: (SpecifierSet) -> List[Tuple[AnyStr, AnyStr]] specs = [] spec = next(iter(specifierset._specs), None) if spec: @@ -365,14 +366,14 @@ def populate_setup_paths(self): base_path = self.base_path if base_path is None: return - setup_paths = get_setup_paths(self.base_path, subdirectory=self.subdirectory) # type: Dict[Text, Optional[Text]] + setup_paths = get_setup_paths(self.base_path, subdirectory=self.subdirectory) # type: Dict[STRING_TYPE, Optional[STRING_TYPE]] self._setup_py = setup_paths.get("setup_py") self._setup_cfg = setup_paths.get("setup_cfg") self._pyproject_toml = setup_paths.get("pyproject_toml") @property def pyproject_requires(self): - # type: () -> Optional[List[Text]] + # type: () -> Optional[List[STRING_TYPE]] if self._pyproject_requires is None and self.pyproject_toml is not None: pyproject_requires, pyproject_backend = get_pyproject(self.path) self._pyproject_requires = pyproject_requires @@ -381,7 +382,7 @@ def pyproject_requires(self): @property def pyproject_backend(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._pyproject_requires is None and self.pyproject_toml is not None: pyproject_requires, pyproject_backend = get_pyproject(self.path) if not pyproject_backend and self.setup_cfg is not None: @@ -493,7 +494,7 @@ def parse_extras(self): self.extras = tuple(sorted(extras)) def get_url(self): - # type: () -> Text + # type: () -> STRING_TYPE """Sets ``self.name`` if given a **PEP-508** style URL""" line = self.line @@ -521,7 +522,7 @@ def get_url(self): @property def name(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._name is None: self.parse_name() if self._name is None and not self.is_named and not self.is_wheel: @@ -531,7 +532,7 @@ def name(self): @name.setter def name(self, name): - # type: (Text) -> None + # type: (STRING_TYPE) -> None self._name = name if self._setup_info: self._setup_info.name = name @@ -542,7 +543,7 @@ def name(self, name): @property def url(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self.uri is not None: url = add_ssh_scheme_to_git_uri(self.uri) else: @@ -567,7 +568,7 @@ def link(self): @property def subdirectory(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self.link is not None: return self.link.subdirectory_fragment return "" @@ -645,7 +646,7 @@ def is_named(self): @property def ref(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._ref is None and self.relpath is not None: self.relpath, self._ref = split_ref_from_uri(self.relpath) return self._ref @@ -778,7 +779,7 @@ def parse_ireq(self): self._ireq.req = self.requirement def _parse_wheel(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if not self.is_wheel: pass from pip_shims.shims import Wheel @@ -789,7 +790,7 @@ def _parse_wheel(self): return name def _parse_name_from_link(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self.link is None: return None @@ -800,7 +801,7 @@ def _parse_name_from_link(self): return None def _parse_name_from_line(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if not self.is_named: pass @@ -963,13 +964,13 @@ def parse_markers(self): @property def requirement_info(self): - # type: () -> Tuple(Optional[Text], Tuple[Optional[Text]], Optional[Text]) + # type: () -> Tuple[Optional[AnyStr], Tuple[Optional[AnyStr], ...], Optional[AnyStr]] """ Generates a 3-tuple of the requisite *name*, *extras* and *url* to generate a :class:`~packaging.requirements.Requirement` out of. :return: A Tuple containing an optional name, a Tuple of extras names, and an optional URL. - :rtype: Tuple[Optional[Text], Tuple[Optional[Text]], Optional[Text]] + :rtype: Tuple[Optional[AnyStr], Tuple[Optional[AnyStr], ...], Optional[AnyStr]] """ # Direct URLs can be converted to packaging requirements directly, but @@ -1050,10 +1051,10 @@ def parse(self): @attr.s(slots=True, hash=True) class NamedRequirement(object): - name = attr.ib() # type: Text - version = attr.ib() # type: Optional[Text] + name = attr.ib() # type: STRING_TYPE + version = attr.ib() # type: Optional[STRING_TYPE] req = attr.ib() # type: PackagingRequirement - extras = attr.ib(default=attr.Factory(list)) # type: Tuple[Text] + extras = attr.ib(default=attr.Factory(list)) # type: Tuple[STRING_TYPE] editable = attr.ib(default=False) # type: bool _parsed_line = attr.ib(default=None) # type: Optional[Line] @@ -1074,9 +1075,9 @@ def parsed_line(self): @classmethod def from_line(cls, line, parsed_line=None): - # type: (Text, Optional[Line]) -> NamedRequirement + # type: (AnyStr, Optional[Line]) -> NamedRequirement req = init_requirement(line) - specifiers = None # type: Optional[Text] + specifiers = None # type: Optional[STRING_TYPE] if req.specifier: specifiers = specs_to_string(req.specifier) req.line = line @@ -1094,7 +1095,7 @@ def from_line(cls, line, parsed_line=None): "parsed_line": parsed_line, "extras": None } - extras = None # type: Optional[Tuple[Text]] + extras = None # type: Optional[Tuple[STRING_TYPE]] if req.extras: extras = list(req.extras) creation_kwargs["extras"] = extras @@ -1102,13 +1103,13 @@ def from_line(cls, line, parsed_line=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (Text, Dict[Text, Union[Text, Optional[Text], Optional[List[Text]]]]) -> NamedRequirement - creation_args = {} # type: Dict[Text, Union[Optional[Text], Optional[List[Text]]]] + # type: (AnyStr, Dict[AnyStr, Union[Text, str, List[Union[Text, str]]]]) -> NamedRequirement + creation_args = {} # type: Dict[STRING_TYPE, Union[Optional[STRING_TYPE], Optional[List[STRING_TYPE]]]] if hasattr(pipfile, "keys"): attr_fields = [field.name for field in attr.fields(cls)] creation_args = {k: v for k, v in pipfile.items() if k in attr_fields} creation_args["name"] = name - version = get_version(pipfile) # type: Optional[Text] + version = get_version(pipfile) # type: Optional[STRING_TYPE] extras = creation_args.get("extras", None) creation_args["version"] = version req = init_requirement("{0}{1}".format(name, version)) @@ -1119,7 +1120,7 @@ def from_pipfile(cls, name, pipfile): @property def line_part(self): - # type: () -> Text + # type: () -> STRING_TYPE # FIXME: This should actually be canonicalized but for now we have to # simply lowercase it and replace underscores, since full canonicalization # also replaces dots and that doesn't actually work when querying the index @@ -1127,7 +1128,7 @@ def line_part(self): @property def pipfile_part(self): - # type: () -> Dict[Text, Any] + # type: () -> Dict[STRING_TYPE, Any] pipfile_dict = attr.asdict(self, filter=filter_none).copy() # type: ignore if "version" not in pipfile_dict: pipfile_dict["version"] = "*" @@ -1148,36 +1149,36 @@ class FileRequirement(object): containing directories.""" #: Path to the relevant `setup.py` location - setup_path = attr.ib(default=None, cmp=True) # type: Optional[Text] + setup_path = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] #: path to hit - without any of the VCS prefixes (like git+ / http+ / etc) - path = attr.ib(default=None, cmp=True) # type: Optional[Text] + path = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] #: Whether the package is editable editable = attr.ib(default=False, cmp=True) # type: bool #: Extras if applicable - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[Text] - _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[Text] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[STRING_TYPE] + _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] #: URI of the package - uri = attr.ib(cmp=True) # type: Optional[Text] + uri = attr.ib(cmp=True) # type: Optional[STRING_TYPE] #: Link object representing the package to clone link = attr.ib(cmp=True) # type: Optional[Link] #: PyProject Requirements - pyproject_requires = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple + pyproject_requires = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[STRING_TYPE, ...]] #: PyProject Build System - pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[Text] + pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] #: PyProject Path - pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[Text] + pyproject_path = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] #: Setup metadata e.g. dependencies _setup_info = attr.ib(default=None, cmp=True) # type: Optional[SetupInfo] _has_hashed_name = attr.ib(default=False, cmp=True) # type: bool _parsed_line = attr.ib(default=None, cmp=False, hash=True) # type: Optional[Line] #: Package name - name = attr.ib(cmp=True) # type: Optional[Text] + name = attr.ib(cmp=True) # type: Optional[STRING_TYPE] #: A :class:`~pkg_resources.Requirement` isntance req = attr.ib(cmp=True) # type: Optional[PackagingRequirement] @classmethod def get_link_from_line(cls, line): - # type: (Text) -> LinkInfo + # type: (STRING_TYPE) -> LinkInfo """Parse link information from given requirement line. Return a 6-tuple: @@ -1211,16 +1212,16 @@ def get_link_from_line(cls, line): # Git allows `git@github.com...` lines that are not really URIs. # Add "ssh://" so we can parse correctly, and restore afterwards. - fixed_line = add_ssh_scheme_to_git_uri(line) # type: Text + fixed_line = add_ssh_scheme_to_git_uri(line) # type: STRING_TYPE added_ssh_scheme = fixed_line != line # type: bool # We can assume a lot of things if this is a local filesystem path. if "://" not in fixed_line: p = Path(fixed_line).absolute() # type: Path - path = p.as_posix() # type: Optional[Text] - uri = p.as_uri() # type: Text + path = p.as_posix() # type: Optional[STRING_TYPE] + uri = p.as_uri() # type: STRING_TYPE link = create_link(uri) # type: Link - relpath = None # type: Optional[Text] + relpath = None # type: Optional[STRING_TYPE] try: relpath = get_converted_relative_path(path) except ValueError: @@ -1233,13 +1234,13 @@ def get_link_from_line(cls, line): original_url = parsed_url._replace() # type: SplitResult # Split the VCS part out if needed. - original_scheme = parsed_url.scheme # type: Text - vcs_type = None # type: Optional[Text] + original_scheme = parsed_url.scheme # type: STRING_TYPE + vcs_type = None # type: Optional[STRING_TYPE] if "+" in original_scheme: - scheme = None # type: Optional[Text] + scheme = None # type: Optional[STRING_TYPE] vcs_type, _, scheme = original_scheme.partition("+") parsed_url = parsed_url._replace(scheme=scheme) - prefer = "uri" # type: Text + prefer = "uri" # type: STRING_TYPE else: vcs_type = None prefer = "file" @@ -1279,17 +1280,17 @@ def get_link_from_line(cls, line): @property def setup_py_dir(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self.setup_path: return os.path.dirname(os.path.abspath(self.setup_path)) return None @property def dependencies(self): - # type: () -> Tuple[Dict[Text, PackagingRequirement], List[Union[Text, PackagingRequirement]], List[Text]] - build_deps = [] # type: List[Union[Text, PackagingRequirement]] - setup_deps = [] # type: List[Text] - deps = {} # type: Dict[Text, PackagingRequirement] + # type: () -> Tuple[Dict[AnyStr, PackagingRequirement], List[Union[Text, str, PackagingRequirement]], List[AnyStr]] + build_deps = [] # type: List[Union[Text, str, PackagingRequirement]] + setup_deps = [] # type: List[STRING_TYPE] + deps = {} # type: Dict[STRING_TYPE, PackagingRequirement] if self.setup_info: setup_info = self.setup_info.as_dict() deps.update(setup_info.get("requires", {})) @@ -1340,7 +1341,7 @@ def setup_info(self, setup_info): @uri.default def get_uri(self): - # type: () -> Text + # type: () -> STRING_TYPE if self.path and not self.uri: self._uri_scheme = "path" return pip_shims.shims.path_to_url(os.path.abspath(self.path)) @@ -1352,7 +1353,7 @@ def get_uri(self): @name.default def get_name(self): - # type: () -> Text + # type: () -> STRING_TYPE loc = self.path or self.uri if loc and not self._uri_scheme: self._uri_scheme = "path" if self.path else "file" @@ -1519,7 +1520,7 @@ def is_direct_url(self): @property def formatted_path(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self.path: path = self.path if not isinstance(path, Path): @@ -1530,16 +1531,16 @@ def formatted_path(self): @classmethod def create( cls, - path=None, # type: Optional[Text] - uri=None, # type: Text + path=None, # type: Optional[STRING_TYPE] + uri=None, # type: STRING_TYPE editable=False, # type: bool - extras=None, # type: Optional[Tuple[Text]] + extras=None, # type: Optional[Tuple[STRING_TYPE]] link=None, # type: Link vcs_type=None, # type: Optional[Any] - name=None, # type: Optional[Text] + name=None, # type: Optional[STRING_TYPE] req=None, # type: Optional[Any] - line=None, # type: Optional[Text] - uri_scheme=None, # type: Text + line=None, # type: Optional[STRING_TYPE] + uri_scheme=None, # type: STRING_TYPE setup_path=None, # type: Optional[Any] relpath=None, # type: Optional[Any] parsed_line=None, # type: Optional[Line] @@ -1600,7 +1601,7 @@ def create( creation_kwargs["vcs"] = vcs_type if name: creation_kwargs["name"] = name - _line = None # type: Optional[Text] + _line = None # type: Optional[STRING_TYPE] ireq = None # type: Optional[InstallRequirement] setup_info = None # type: Optional[SetupInfo] if parsed_line: @@ -1674,7 +1675,7 @@ def create( @classmethod def from_line(cls, line, extras=None, parsed_line=None): - # type: (Text, Optional[Tuple[Text]], Optional[Line]) -> FileRequirement + # type: (AnyStr, Optional[Tuple[AnyStr, ...]], Optional[Line]) -> FileRequirement line = line.strip('"').strip("'") link = None path = None @@ -1725,7 +1726,7 @@ def from_line(cls, line, extras=None, parsed_line=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (Text, Dict[Text, Any]) -> FileRequirement + # type: (AnyStr, Dict[AnyStr, Any]) -> FileRequirement # Parse the values out. After this dance we should have two variables: # path - Local filesystem path. # uri - Absolute URI that is parsable with urlsplit. @@ -1797,9 +1798,9 @@ def from_pipfile(cls, name, pipfile): @property def line_part(self): - # type: () -> Text - link_url = None # type: Optional[Text] - seed = None # type: Optional[Text] + # type: () -> STRING_TYPE + link_url = None # type: Optional[STRING_TYPE] + seed = None # type: Optional[STRING_TYPE] if self.link is not None: link_url = unquote(self.link.url_without_fragment) if self._uri_scheme and self._uri_scheme == "path": @@ -1819,7 +1820,7 @@ def line_part(self): @property def pipfile_part(self): - # type: () -> Dict[Text, Dict[Text, Any]] + # type: () -> Dict[AnyStr, Dict[AnyStr, Any]] excludes = [ "_base_line", "_has_hashed_name", "setup_path", "pyproject_path", "_uri_scheme", "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" @@ -1838,7 +1839,7 @@ def pipfile_part(self): pipfile_dict.pop("_uri_scheme") # For local paths and remote installable artifacts (zipfiles, etc) collision_keys = {"file", "uri", "path"} - collision_order = ["file", "uri", "path"] # type: List[Text] + collision_order = ["file", "uri", "path"] # type: List[STRING_TYPE] key_match = next(iter(k for k in collision_order if k in pipfile_dict.keys())) if self._uri_scheme: dict_key = self._uri_scheme @@ -1880,18 +1881,18 @@ class VCSRequirement(FileRequirement): #: Whether the repository is editable editable = attr.ib(default=None) # type: Optional[bool] #: URI for the repository - uri = attr.ib(default=None) # type: Optional[Text] + uri = attr.ib(default=None) # type: Optional[STRING_TYPE] #: path to the repository, if it's local - path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) # type: Optional[Text] + path = attr.ib(default=None, validator=attr.validators.optional(validate_path)) # type: Optional[STRING_TYPE] #: vcs type, i.e. git/hg/svn - vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) # type: Optional[Text] + vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None) # type: Optional[STRING_TYPE] #: vcs reference name (branch / commit / tag) - ref = attr.ib(default=None) # type: Optional[Text] + ref = attr.ib(default=None) # type: Optional[STRING_TYPE] #: Subdirectory to use for installation if applicable - subdirectory = attr.ib(default=None) # type: Optional[Text] + subdirectory = attr.ib(default=None) # type: Optional[STRING_TYPE] _repo = attr.ib(default=None) # type: Optional[VCSRepository] - _base_line = attr.ib(default=None) # type: Optional[Text] - name = attr.ib() # type: Text + _base_line = attr.ib(default=None) # type: Optional[STRING_TYPE] + name = attr.ib() # type: STRING_TYPE link = attr.ib() # type: Optional[pip_shims.shims.Link] req = attr.ib() # type: Optional[RequirementType] @@ -1927,7 +1928,7 @@ def get_link(self): @name.default def get_name(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] return ( self.link.egg_fragment or self.req.name if getattr(self, "req", None) @@ -1936,7 +1937,7 @@ def get_name(self): @property def vcs_uri(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] uri = self.uri if not any(uri.startswith("{0}+".format(vcs)) for vcs in VCS_LIST): uri = "{0}+{1}".format(self.vcs, uri) @@ -2028,7 +2029,7 @@ def repo(self): return self._repo def get_checkout_dir(self, src_dir=None): - # type: (Optional[Text]) -> Text + # type: (Optional[AnyStr]) -> AnyStr src_dir = os.environ.get("PIP_SRC", None) if not src_dir else src_dir checkout_dir = None if self.is_local: @@ -2045,7 +2046,7 @@ def get_checkout_dir(self, src_dir=None): return os.path.join(create_tracked_tempdir(prefix="requirementslib"), self.name) def get_vcs_repo(self, src_dir=None, checkout_dir=None): - # type: (Optional[Text], Optional[Text]) -> VCSRepository + # type: (Optional[AnyStr], Optional[AnyStr]) -> VCSRepository from .vcs import VCSRepository if checkout_dir is None: @@ -2076,13 +2077,13 @@ def get_vcs_repo(self, src_dir=None, checkout_dir=None): return vcsrepo def get_commit_hash(self): - # type: () -> Text + # type: () -> STRING_TYPE hash_ = None hash_ = self.repo.get_commit_hash() return hash_ def update_repo(self, src_dir=None, ref=None): - # type: (Optional[Text], Optional[Text]) -> Text + # type: (Optional[AnyStr], Optional[AnyStr]) -> AnyStr if ref: self.ref = ref else: @@ -2097,7 +2098,7 @@ def update_repo(self, src_dir=None, ref=None): @contextmanager def locked_vcs_repo(self, src_dir=None): - # type: (Optional[Text]) -> Generator[VCSRepository, None, None] + # type: (Optional[AnyStr]) -> Generator[VCSRepository, None, None] if not src_dir: src_dir = create_tracked_tempdir(prefix="requirementslib-", suffix="-src") vcsrepo = self.get_vcs_repo(src_dir=src_dir) @@ -2138,7 +2139,7 @@ def locked_vcs_repo(self, src_dir=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (Text, Dict[Text, Union[List[Text], Text, bool]]) -> VCSRequirement + # type: (AnyStr, Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]]) -> VCSRequirement creation_args = {} pipfile_keys = [ k @@ -2186,7 +2187,7 @@ def from_pipfile(cls, name, pipfile): @classmethod def from_line(cls, line, editable=None, extras=None, parsed_line=None): - # type: (Text, Optional[bool], Optional[Tuple[Text]], Optional[Line]) -> VCSRequirement + # type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr]], Optional[Line]) -> VCSRequirement relpath = None if parsed_line is None: parsed_line = Line(line) @@ -2264,7 +2265,7 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): @property def line_part(self): - # type: () -> Text + # type: () -> STRING_TYPE """requirements.txt compatible line part sans-extras""" if self.is_local: base_link = self.link @@ -2295,7 +2296,7 @@ def line_part(self): @staticmethod def _choose_vcs_source(pipfile): - # type: (Dict[Text, Union[List[Text], Text, bool]]) -> Dict[Text, Union[List[Text], Text, bool]] + # type: (Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]]) -> Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]] src_keys = [k for k in pipfile.keys() if k in ["path", "uri", "file"]] if src_keys: chosen_key = first(src_keys) @@ -2308,7 +2309,7 @@ def _choose_vcs_source(pipfile): @property def pipfile_part(self): - # type: () -> Dict[Text, Dict[Text, Union[List[Text], Text, bool]]] + # type: () -> Dict[AnyStr, Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]]] excludes = [ "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line", @@ -2332,15 +2333,15 @@ def pipfile_part(self): @attr.s(cmp=True, hash=True) class Requirement(object): - _name = attr.ib(cmp=True) # type: Text - vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[Text] + _name = attr.ib(cmp=True) # type: STRING_TYPE + vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs), cmp=True) # type: Optional[STRING_TYPE] req = attr.ib(default=None, cmp=True) # type: Optional[Union[VCSRequirement, FileRequirement, NamedRequirement]] - markers = attr.ib(default=None, cmp=True) # type: Optional[Text] - _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[Text] - index = attr.ib(default=None, cmp=True) # type: Optional[Text] + markers = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] + _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[STRING_TYPE] + index = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] editable = attr.ib(default=None, cmp=True) # type: Optional[bool] - hashes = attr.ib(factory=frozenset, converter=frozenset, cmp=True) # type: Optional[Tuple[Text]] - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[Text]] + hashes = attr.ib(factory=frozenset, converter=frozenset, cmp=True) # type: Optional[Tuple[STRING_TYPE]] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[STRING_TYPE]] abstract_dep = attr.ib(default=None, cmp=False) # type: Optional[AbstractDependency] _line_instance = attr.ib(default=None, cmp=False) # type: Optional[Line] _ireq = attr.ib(default=None, cmp=False) # type: Optional[pip_shims.InstallRequirement] @@ -2350,12 +2351,14 @@ def __hash__(self): @_name.default def get_name(self): - # type: () -> Optional[Text] - return self.req.name + # type: () -> Optional[STRING_TYPE] + if self.req is not None: + return self.req.name + return None @property def name(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._name is not None: return self._name name = None @@ -2369,7 +2372,9 @@ def name(self): @property def requirement(self): # type: () -> Optional[PackagingRequirement] - return self.req.req + if self.req: + return self.req.req + return None def add_hashes(self, hashes): # type: (Union[List, Set, Tuple]) -> Requirement @@ -2380,7 +2385,7 @@ def add_hashes(self, hashes): return attr.evolve(self, hashes=frozenset(new_hashes)) def get_hashes_as_pip(self, as_list=False): - # type: (bool) -> Union[Text, List[Text]] + # type: (bool) -> Union[STRING_TYPE, List[STRING_TYPE]] if self.hashes: if as_list: return [HASH_STRING.format(h) for h in self.hashes] @@ -2389,12 +2394,12 @@ def get_hashes_as_pip(self, as_list=False): @property def hashes_as_pip(self): - # type: () -> Union[Text, List[Text]] - self.get_hashes_as_pip() + # type: () -> Union[Text, str, List[AnyStr]] + return self.get_hashes_as_pip() @property def markers_as_pip(self): - # type: () -> Text + # type: () -> STRING_TYPE if self.markers: return " ; {0}".format(self.markers).replace('"', "'") @@ -2402,27 +2407,28 @@ def markers_as_pip(self): @property def extras_as_pip(self): - # type: () -> Text + # type: () -> STRING_TYPE if self.extras: return "[{0}]".format( - ",".join(sorted([extra.lower() for extra in self.extras])) + ",".join(sorted([extra.lower() for extra in self.extras])) # type: ignore ) return "" @cached_property def commit_hash(self): - # type: () -> Optional[Text] - if not self.is_vcs: + # type: () -> Optional[STRING_TYPE] + if self.req is None or not isinstance(self.req, VCSRequirement): return None commit_hash = None - with self.req.locked_vcs_repo() as repo: - commit_hash = repo.get_commit_hash() + if self.req is not None: + with self.req.locked_vcs_repo() as repo: + commit_hash = repo.get_commit_hash() return commit_hash @_specifiers.default def get_specifiers(self): - # type: () -> Text + # type: () -> STRING_TYPE if self.req and self.req.req and self.req.req.specifier: return specs_to_string(self.req.req.specifier) return "" @@ -2449,8 +2455,8 @@ def update_name_from_path(self, path): def line_instance(self): # type: () -> Optional[Line] if self._line_instance is None: - if self.req._parsed_line is not None: - self._line_instance = self.req.parsed_line + if self.req is not None and self.req._parsed_line is not None: + self._line_instance = self.req._parsed_line else: include_extras = True include_specifiers = True @@ -2458,16 +2464,17 @@ def line_instance(self): include_extras = False if self.is_file_or_url or self.is_vcs or not self._specifiers: include_specifiers = False - + line_part = "" + if self.req and self.req.line_part: + line_part = self.req.line_part + parts = [] # type: List[STRING_TYPE] parts = [ - self.req.line_part, + line_part, self.extras_as_pip if include_extras else "", self._specifiers if include_specifiers else "", self.markers_as_pip, ] line = "".join(parts) - if line is None: - return None self._line_instance = Line(line) return self._line_instance @@ -2480,7 +2487,7 @@ def line_instance(self, line_instance): @property def specifiers(self): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] if self._specifiers: return self._specifiers else: @@ -2488,28 +2495,36 @@ def specifiers(self): if specs: self._specifiers = specs return specs - if self.is_named and not self._specifiers: + if not self._specifiers and ( + self.req is not None and + isinstance(self.req, NamedRequirement) and + self.req.version + ): self._specifiers = self.req.version - elif not self.editable and not self.is_named: + elif not self.editable and self.req and not isinstance(self.req, NamedRequirement): if self.line_instance and self.line_instance.setup_info and self.line_instance.setup_info.version: self._specifiers = "=={0}".format(self.req.setup_info.version) - elif self.req.parsed_line.specifiers and not self._specifiers: - self._specifiers = specs_to_string(self.req.parsed_line.specifiers) - elif self.line_instance.specifiers and not self._specifiers: - self._specifiers = specs_to_string(self.line_instance.specifiers) - elif not self._specifiers and (self.is_file_or_url or self.is_vcs): - try: - setupinfo_dict = self.run_requires() - except Exception: - setupinfo_dict = None - if setupinfo_dict is not None: - self._specifiers = "=={0}".format(setupinfo_dict.get("version")) + elif not self._specifiers: + if self.req and self.req.parsed_line and self.req.parsed_line.specifiers: + self._specifiers = specs_to_string(self.req.parsed_line.specifiers) + elif self.line_instance and self.line_instance.specifiers: + self._specifiers = specs_to_string(self.line_instance.specifiers) + elif self.is_file_or_url or self.is_vcs: + try: + setupinfo_dict = self.run_requires() + except Exception: + setupinfo_dict = None + if setupinfo_dict is not None: + self._specifiers = "=={0}".format(setupinfo_dict.get("version")) if self._specifiers: specset = SpecifierSet(self._specifiers) if self.line_instance and not self.line_instance.specifiers: self.line_instance.specifiers = specset - if self.req and self.req.parsed_line and not self.req.parsed_line.specifiers: - self.req._parsed_line.specifiers = specset + if self.req: + if self.req._parsed_line and not self.req._parsed_line.specifiers: + self.req._parsed_line.specifiers = specset + elif not self.req._parsed_line and self.line_instance: + self.req._parsed_line = self.line_instance if self.req and self.req.req and not self.req.req.specifier: self.req.req.specifier = specset return self._specifiers @@ -2521,9 +2536,10 @@ def is_vcs(self): @property def build_backend(self): - # type: () -> Optional[Text] - if self.is_vcs or (self.is_file_or_url and ( - self.req is not None and self.req.is_local)): + # type: () -> Optional[STRING_TYPE] + if self.req is not None and ( + not isinstance(self.req, NamedRequirement) and self.req.is_local + ): setup_info = self.run_requires() build_backend = setup_info.get("build_backend") return build_backend @@ -2549,8 +2565,7 @@ def is_named(self): @property def is_wheel(self): # type: () -> bool - if not self.is_named and ( - self.req is not None and + if self.req and not isinstance(self.req, NamedRequirement) and ( self.req.link is not None and self.req.link.is_wheel ): @@ -2559,7 +2574,7 @@ def is_wheel(self): @property def normalized_name(self): - # type: () -> Text + # type: () -> STRING_TYPE return canonicalize_name(self.name) def copy(self): @@ -2568,7 +2583,7 @@ def copy(self): @classmethod @lru_cache() def from_line(cls, line): - # type: (Text) -> Requirement + # type: (AnyStr) -> Requirement if isinstance(line, pip_shims.shims.InstallRequirement): line = format_requirement(line) parsed_line = Line(line) @@ -2597,6 +2612,7 @@ def from_line(cls, line): "line_instance": parsed_line } if parsed_line.extras: + extras = () # type: Tuple[STRING_TYPE, ...] extras = tuple(sorted(dedup([extra.lower() for extra in parsed_line.extras]))) args["extras"] = extras if r is not None: @@ -2947,24 +2963,26 @@ def merge_markers(self, markers): def file_req_from_parsed_line(parsed_line): # type: (Line) -> FileRequirement path = parsed_line.relpath if parsed_line.relpath else parsed_line.path - pyproject_requires = () # type: Tuple[Text] + pyproject_requires = None # type: Optional[Tuple[AnyStr, ...]] if parsed_line.pyproject_requires is not None: pyproject_requires = tuple(parsed_line.pyproject_requires) - return FileRequirement( - setup_path=parsed_line.setup_py, - path=path, - editable=parsed_line.editable, - extras=parsed_line.extras, - uri_scheme=parsed_line.preferred_scheme, - link=parsed_line.link, - uri=parsed_line.uri, - pyproject_requires=pyproject_requires, - pyproject_backend=parsed_line.pyproject_backend, - pyproject_path=Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, - parsed_line=parsed_line, - name=parsed_line.name, - req=parsed_line.requirement - ) + req_dict = { + "setup_path": parsed_line.setup_py, + "path": path, + "editable": parsed_line.editable, + "extras": parsed_line.extras, + "uri_scheme": parsed_line.preferred_scheme, + "link": parsed_line.link, + "uri": parsed_line.uri, + "pyproject_requires": pyproject_requires, + "pyproject_backend": parsed_line.pyproject_backend, + "pyproject_path": Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, + "parsed_line": parsed_line, + "req": parsed_line.requirement + } + if parsed_line.name is not None: + req_dict["name"] = parsed_line.name + return FileRequirement(**req_dict) # type: ignore def vcs_req_from_parsed_line(parsed_line): @@ -2983,37 +3001,41 @@ def vcs_req_from_parsed_line(parsed_line): )) else: link = parsed_line.link - pyproject_requires = () # type: Tuple[Text] + pyproject_requires = () # type: Optional[Tuple[AnyStr, ...]] if parsed_line.pyproject_requires is not None: pyproject_requires = tuple(parsed_line.pyproject_requires) - return VCSRequirement( - setup_path=parsed_line.setup_py, - path=parsed_line.path, - editable=parsed_line.editable, - vcs=parsed_line.vcs, - ref=parsed_line.ref, - subdirectory=parsed_line.subdirectory, - extras=parsed_line.extras, - uri_scheme=parsed_line.preferred_scheme, - link=link, - uri=parsed_line.uri, - pyproject_requires=pyproject_requires, - pyproject_backend=parsed_line.pyproject_backend, - pyproject_path=Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, - parsed_line=parsed_line, - name=parsed_line.name, - req=parsed_line.requirement, - base_line=line, - ) + vcs_dict = { + "setup_path": parsed_line.setup_py, + "path": parsed_line.path, + "editable": parsed_line.editable, + "vcs": parsed_line.vcs, + "ref": parsed_line.ref, + "subdirectory": parsed_line.subdirectory, + "extras": parsed_line.extras, + "uri_scheme": parsed_line.preferred_scheme, + "link": link, + "uri": parsed_line.uri, + "pyproject_requires": pyproject_requires, + "pyproject_backend": parsed_line.pyproject_backend, + "pyproject_path": Path(parsed_line.pyproject_toml) if parsed_line.pyproject_toml else None, + "parsed_line": parsed_line, + "req": parsed_line.requirement, + "base_line": line + } + if parsed_line.name: + vcs_dict["name"] = parsed_line.name + return VCSRequirement(**vcs_dict) # type: ignore def named_req_from_parsed_line(parsed_line): # type: (Line) -> NamedRequirement - return NamedRequirement( - name=parsed_line.name, - version=parsed_line.specifier, - req=parsed_line.requirement, - extras=parsed_line.extras, - editable=parsed_line.editable, - parsed_line=parsed_line - ) + if parsed_line.name is not None: + return NamedRequirement( + name=parsed_line.name, + version=parsed_line.specifier, + req=parsed_line.requirement, + extras=parsed_line.extras, + editable=parsed_line.editable, + parsed_line=parsed_line + ) + return NamedRequirement.from_line(parsed_line.line) diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index 387997f3..93b1506c 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -31,7 +31,7 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Union, Optional, List, Set, Any, TypeVar, Tuple, Sequence, Dict, Text + from typing import Union, Optional, List, Set, Any, TypeVar, Tuple, Sequence, Dict, Text, AnyStr from attr import _ValidatorType from packaging.requirements import Requirement as PackagingRequirement from pkg_resources import Requirement as PkgResourcesRequirement @@ -48,6 +48,7 @@ TOp = TypeVar("TOp", PkgResourcesOp, Op) MarkerTuple = Tuple[TVariable, TOp, TValue] TRequirement = Union[PackagingRequirement, PkgResourcesRequirement] + STRING_TYPE = Union[bytes, str, Text] HASH_STRING = " --hash={0}" @@ -69,7 +70,7 @@ def filter_none(k, v): - # type: (Text, Any) -> bool + # type: (AnyStr, Any) -> bool if v: return True return False @@ -81,7 +82,7 @@ def optional_instance_of(cls): def create_link(link): - # type: (Text) -> Link + # type: (AnyStr) -> Link if not isinstance(link, six.string_types): raise TypeError("must provide a string to instantiate a new link") @@ -90,7 +91,7 @@ def create_link(link): def get_url_name(url): - # type: (Text) -> Text + # type: (AnyStr) -> AnyStr """ Given a url, derive an appropriate name to use in a pipfile. @@ -104,7 +105,7 @@ def get_url_name(url): def init_requirement(name): - # type: (Text) -> TRequirement + # type: (AnyStr) -> TRequirement if not isinstance(name, six.string_types): raise TypeError("must supply a name to generate a requirement") @@ -131,7 +132,7 @@ def extras_to_string(extras): def parse_extras(extras_str): - # type: (Text) -> List + # type: (AnyStr) -> List """ Turn a string of extras into a parsed extras list """ @@ -142,7 +143,7 @@ def parse_extras(extras_str): def specs_to_string(specs): - # type: (List[Union[Text, Specifier]]) -> Text + # type: (List[Union[STRING_TYPE, Specifier]]) -> AnyStr """ Turn a list of specifier tuples into a string """ @@ -159,14 +160,14 @@ def specs_to_string(specs): def build_vcs_uri( - vcs, # type: Optional[Text] - uri, # type: Text - name=None, # type: Optional[Text] - ref=None, # type: Optional[Text] - subdirectory=None, # type: Optional[Text] - extras=None # type: Optional[List[Text]] + vcs, # type: Optional[STRING_TYPE] + uri, # type: STRING_TYPE + name=None, # type: Optional[STRING_TYPE] + ref=None, # type: Optional[STRING_TYPE] + subdirectory=None, # type: Optional[STRING_TYPE] + extras=None # type: Optional[List[STRING_TYPE]] ): - # type: (...) -> Text + # type: (...) -> STRING_TYPE if extras is None: extras = [] vcs_start = "" @@ -187,14 +188,14 @@ def build_vcs_uri( def convert_direct_url_to_url(direct_url): - # type: (Text) -> Text + # type: (AnyStr) -> AnyStr """ Given a direct url as defined by *PEP 508*, convert to a :class:`~pip_shims.shims.Link` compatible URL by moving the name and extras into an **egg_fragment**. :param str direct_url: A pep-508 compliant direct url. :return: A reformatted URL for use with Link objects and :class:`~pip_shims.shims.InstallRequirement` objects. - :rtype: Text + :rtype: AnyStr """ direct_match = DIRECT_URL_RE.match(direct_url) if direct_match is None: @@ -218,15 +219,15 @@ def convert_direct_url_to_url(direct_url): def convert_url_to_direct_url(url, name=None): - # type: (Text, Optional[Text]) -> Text + # type: (AnyStr, Optional[AnyStr]) -> AnyStr """ Given a :class:`~pip_shims.shims.Link` compatible URL, convert to a direct url as defined by *PEP 508* by extracting the name and extras from the **egg_fragment**. - :param Text url: A :class:`~pip_shims.shims.InstallRequirement` compliant URL. - :param Optiona[Text] name: A name to use in case the supplied URL doesn't provide one. + :param AnyStr url: A :class:`~pip_shims.shims.InstallRequirement` compliant URL. + :param Optiona[AnyStr] name: A name to use in case the supplied URL doesn't provide one. :return: A pep-508 compliant direct url. - :rtype: Text + :rtype: AnyStr :raises ValueError: Raised when the URL can't be parsed or a name can't be found. :raises TypeError: When a non-string input is provided. @@ -266,7 +267,7 @@ def convert_url_to_direct_url(url, name=None): def get_version(pipfile_entry): - # type: (Union[Text, Dict[Text, bool, List[Text]]]) -> Text + # type: (Union[STRING_TYPE, Dict[AnyStr, bool, List[AnyStr]]]) -> AnyStr if str(pipfile_entry) == "{}" or is_star(pipfile_entry): return "" @@ -327,7 +328,7 @@ def _strip_extras_markers(marker): @lru_cache() def get_setuptools_version(): - # type: () -> Optional[Text] + # type: () -> Optional[STRING_TYPE] import pkg_resources setuptools_dist = pkg_resources.get_distribution( pkg_resources.Requirement("setuptools") @@ -336,7 +337,7 @@ def get_setuptools_version(): def get_default_pyproject_backend(): - # type: () -> Text + # type: () -> STRING_TYPE st_version = get_setuptools_version() if st_version is not None: parsed_st_version = parse_version(st_version) @@ -346,14 +347,14 @@ def get_default_pyproject_backend(): def get_pyproject(path): - # type: (Union[Text, Path]) -> Tuple[List[Text], Text] + # type: (Union[STRING_TYPE, Path]) -> Tuple[List[STRING_TYPE], STRING_TYPE] """ Given a base path, look for the corresponding ``pyproject.toml`` file and return its build_requires and build_backend. - :param Text path: The root path of the project, should be a directory (will be truncated) + :param AnyStr path: The root path of the project, should be a directory (will be truncated) :return: A 2 tuple of build requirements and the build backend - :rtype: Tuple[List[Text], Text] + :rtype: Tuple[List[AnyStr], AnyStr] """ if not path: @@ -394,7 +395,7 @@ def get_pyproject(path): def split_markers_from_line(line): - # type: (Text) -> Tuple[Text, Optional[Text]] + # type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]] """Split markers from a dependency""" if not any(line.startswith(uri_prefix) for uri_prefix in SCHEME_LIST): marker_sep = ";" @@ -408,7 +409,7 @@ def split_markers_from_line(line): def split_vcs_method_from_uri(uri): - # type: (Text) -> Tuple[Optional[Text], Text] + # type: (AnyStr) -> Tuple[Optional[AnyStr], AnyStr] """Split a vcs+uri formatted uri into (vcs, uri)""" vcs_start = "{0}+" vcs = first([vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))]) @@ -418,14 +419,14 @@ def split_vcs_method_from_uri(uri): def split_ref_from_uri(uri): - # type: (Text) -> Tuple[Text, Optional[Text]] + # type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]] """ Given a path or URI, check for a ref and split it from the path if it is present, returning a tuple of the original input and the ref or None. - :param Text uri: The path or URI to split + :param AnyStr uri: The path or URI to split :returns: A 2-tuple of the path or URI and the ref - :rtype: Tuple[Text, Optional[Text]] + :rtype: Tuple[AnyStr, Optional[AnyStr]] """ if not isinstance(uri, six.string_types): raise TypeError("Expected a string, received {0!r}".format(uri)) @@ -818,12 +819,12 @@ def fix_requires_python_marker(requires_python): def normalize_name(pkg): - # type: (Text) -> Text + # type: (AnyStr) -> AnyStr """Given a package name, return its normalized, non-canonicalized form. - :param Text pkg: The name of a package + :param AnyStr pkg: The name of a package :return: A normalized package name - :rtype: Text + :rtype: AnyStr """ assert isinstance(pkg, six.string_types) @@ -831,12 +832,12 @@ def normalize_name(pkg): def get_name_variants(pkg): - # type: (Text) -> Set[Text] + # type: (STRING_TYPE) -> Set[STRING_TYPE] """ Given a packager name, get the variants of its name for both the canonicalized and "safe" forms. - :param Text pkg: The package to lookup + :param AnyStr pkg: The package to lookup :returns: A list of names. :rtype: Set """ From db28878f1a58647dc4db05e00b049397f12c4ad0 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 27 Feb 2019 23:40:50 -0500 Subject: [PATCH 32/35] Update type hints Signed-off-by: Dan Ryan --- src/requirementslib/models/dependencies.py | 18 +- src/requirementslib/models/requirements.py | 291 +++++++++++---------- src/requirementslib/models/setup_info.py | 89 ++++--- src/requirementslib/models/utils.py | 27 +- 4 files changed, 229 insertions(+), 196 deletions(-) diff --git a/src/requirementslib/models/dependencies.py b/src/requirementslib/models/dependencies.py index f87fd585..44f34edb 100644 --- a/src/requirementslib/models/dependencies.py +++ b/src/requirementslib/models/dependencies.py @@ -20,6 +20,7 @@ from vistir.misc import partialclass from vistir.path import create_tracked_tempdir +from ..environment import MYPY_RUNNING from ..utils import prepare_pip_source_args, _ensure_dir from .cache import CACHE_DIR, DependencyCache from .utils import ( @@ -29,6 +30,17 @@ ) +if MYPY_RUNNING: + from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text, Set, AnyStr + from pip_shims.shims import InstallRequirement, InstallationCandidate, PackageFinder, Command + from packaging.requirements import Requirement as PackagingRequirement + TRequirement = TypeVar("TRequirement") + RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) + MarkerType = TypeVar('MarkerType', covariant=True, bound=Marker) + STRING_TYPE = Union[str, bytes, Text] + S = TypeVar("S", bytes, str, Text) + + PKGS_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "pkgs")) WHEEL_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "wheels")) @@ -43,6 +55,7 @@ def _get_filtered_versions(ireq, versions, prereleases): def find_all_matches(finder, ireq, pre=False): + # type: (PackageFinder, InstallRequirement, bool) -> List[InstallationCandidate] """Find all matching dependencies using the supplied finder and the given ireq. @@ -65,6 +78,7 @@ def find_all_matches(finder, ireq, pre=False): def get_pip_command(): + # type: () -> Command # Use pip's parser for pip.conf management and defaults. # General options (find_links, index_url, extra_index_url, trusted_host, # and pre) are defered to pip. @@ -89,7 +103,7 @@ class PipCommand(pip_shims.shims.Command): @attr.s class AbstractDependency(object): - name = attr.ib() + name = attr.ib() # type: STRING_TYPE specifiers = attr.ib() markers = attr.ib() candidates = attr.ib() @@ -284,6 +298,7 @@ def get_abstract_dependencies(reqs, sources=None, parent=None): def get_dependencies(ireq, sources=None, parent=None): + # type: (Union[InstallRequirement, InstallationCandidate], Optional[List[Dict[S, Union[S, bool]]]], Optional[AbstractDependency]) -> Set[S, ...] """Get all dependencies for a given install requirement. :param ireq: A single InstallRequirement @@ -556,6 +571,7 @@ def get_pip_options(args=[], sources=None, pip_command=None): def get_finder(sources=None, pip_command=None, pip_options=None): + # type: (List[Dict[S, Union[S, bool]]], Optional[Command], Any) -> PackageFinder """Get a package finder for looking up candidates to install :param sources: A list of pipfile-formatted sources, defaults to None diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 60320bdb..4959353d 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -78,12 +78,13 @@ if MYPY_RUNNING: from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Set, AnyStr, Text - from pip_shims.shims import Link, InstallRequirement + from pip_shims.shims import Link, InstallRequirement, PackageFinder, InstallationCandidate RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) from six.moves.urllib.parse import SplitResult from .vcs import VCSRepository NON_STRING_ITERABLE = Union[List, Set, Tuple] STRING_TYPE = Union[str, bytes, Text] + S = TypeVar("S", bytes, str, Text) SPECIFIERS_BY_LENGTH = sorted(list(Specifier._operators.keys()), key=len, reverse=True) @@ -94,12 +95,12 @@ class Line(object): def __init__(self, line, extras=None): - # type: (AnyStr, Optional[NON_STRING_ITERABLE]) -> None + # type: (AnyStr, Optional[Union[List[S], Set[S], Tuple[S, ...]]]) -> None self.editable = False # type: bool if line.startswith("-e "): line = line[len("-e "):] self.editable = True - self.extras = () # type: Tuple[STRING_TYPE] + self.extras = () # type: Tuple[STRING_TYPE, ...] if extras is not None: self.extras = tuple(sorted(set(extras))) self.line = line # type: STRING_TYPE @@ -123,7 +124,7 @@ def __init__(self, line, extras=None): self._pyproject_toml = None # type: Optional[STRING_TYPE] self._pyproject_requires = None # type: Optional[Tuple[STRING_TYPE, ...]] self._pyproject_backend = None # type: Optional[STRING_TYPE] - self._wheel_kwargs = None # type: Dict[STRING_TYPE, STRING_TYPE] + self._wheel_kwargs = None # type: Optional[Dict[STRING_TYPE, STRING_TYPE]] self._vcsrepo = None # type: Optional[VCSRepository] self._setup_info = None # type: Optional[SetupInfo] self._ref = None # type: Optional[STRING_TYPE] @@ -148,18 +149,19 @@ def __repr__(self): "pyproject_requires={self._pyproject_requires}, " "pyproject_backend={self._pyproject_backend}, ireq={self._ireq})>".format( self=self - )) + ) + ) except Exception: return "".format(self.__dict__.values()) @classmethod def split_hashes(cls, line): - # type: (AnyStr) -> Tuple[AnyStr, List[AnyStr]] + # type: (S) -> Tuple[S, List[S]] if "--hash" not in line: return line, [] split_line = line.split() - line_parts = [] # type: List[STRING_TYPE] - hashes = [] # type: List[STRING_TYPE] + line_parts = [] # type: List[S] + hashes = [] # type: List[S] for part in split_line: if part.startswith("--hash"): param, _, value = part.partition("=") @@ -176,8 +178,6 @@ def line_with_prefix(self): extras_str = extras_to_string(self.extras) if self.is_direct_url: line = self.link.url - # if self.link.egg_info and self.extras: - # line = "{0}{1}".format(line, extras_str) elif extras_str: if self.is_vcs: line = self.link.url @@ -192,7 +192,7 @@ def line_with_prefix(self): @property def line_for_ireq(self): # type: () -> STRING_TYPE - line = "" + line = "" # type: STRING_TYPE if self.is_file or self.is_url and not self.is_vcs: scheme = self.preferred_scheme if self.preferred_scheme is not None else "uri" local_line = next(iter([ @@ -232,7 +232,7 @@ def line_for_ireq(self): @property def base_path(self): - # type: () -> Optional[STRING_TYPE] + # type: () -> Optional[S] if not self.link and not self.path: self.parse_link() if not self.path: @@ -274,14 +274,16 @@ def specifier(self): for req in (self.ireq, self.requirement): if req is not None and getattr(req, "specifier", None): options.append(req.specifier) - specifier = next(iter(spec for spec in options if spec is not None), None) + specifier = next(iter(spec for spec in options if spec is not None), None) # type: Optional[Union[Specifier, SpecifierSet]] + spec_string = None # type: Optional[STRING_TYPE] if specifier is not None: - specifier = specs_to_string(specifier) - elif specifier is None and not self.is_named and self._setup_info is not None: - if self._setup_info.version: - specifier = "=={0}".format(self._setup_info.version) - if specifier: - self._specifier = specifier + spec_string = specs_to_string(specifier) + elif specifier is None and not self.is_named and ( + self._setup_info is not None and self._setup_info.version + ): + spec_string = "=={0}".format(self._setup_info.version) + if spec_string: + self._specifier = spec_string return self._specifier @specifier.setter @@ -327,7 +329,7 @@ def specifiers(self, specifiers): else: raise TypeError("Must pass a string or a SpecifierSet") specs = self.get_requirement_specs(specifiers) - if self.ireq is not None and self.ireq.req is not None: + if self.ireq is not None and self._ireq and self._ireq.req is not None: self._ireq.req.specifier = specifiers self._ireq.req.specs = specs if self.requirement is not None: @@ -366,17 +368,18 @@ def populate_setup_paths(self): base_path = self.base_path if base_path is None: return - setup_paths = get_setup_paths(self.base_path, subdirectory=self.subdirectory) # type: Dict[STRING_TYPE, Optional[STRING_TYPE]] + setup_paths = get_setup_paths(base_path, subdirectory=self.subdirectory) # type: Dict[STRING_TYPE, Optional[STRING_TYPE]] self._setup_py = setup_paths.get("setup_py") self._setup_cfg = setup_paths.get("setup_cfg") self._pyproject_toml = setup_paths.get("pyproject_toml") @property def pyproject_requires(self): - # type: () -> Optional[List[STRING_TYPE]] + # type: () -> Optional[Tuple[STRING_TYPE, ...]] if self._pyproject_requires is None and self.pyproject_toml is not None: pyproject_requires, pyproject_backend = get_pyproject(self.path) - self._pyproject_requires = pyproject_requires + if pyproject_requires: + self._pyproject_requires = tuple(pyproject_requires) self._pyproject_backend = pyproject_backend return self._pyproject_requires @@ -388,9 +391,9 @@ def pyproject_backend(self): if not pyproject_backend and self.setup_cfg is not None: setup_dict = SetupInfo.get_setup_cfg(self.setup_cfg) pyproject_backend = get_default_pyproject_backend() - pyproject_requires = setup_dict.get("build_requires", ["setuptools", "wheel"]) + pyproject_requires = setup_dict.get("build_requires", ["setuptools", "wheel"]) # type: ignore - self._pyproject_requires = pyproject_requires + self._pyproject_requires = tuple(pyproject_requires) self._pyproject_backend = pyproject_backend return self._pyproject_backend @@ -415,6 +418,7 @@ def parse_extras(self): """ extras = None + url = "" # type: STRING_TYPE if "@" in self.line or self.is_vcs or self.is_url: line = "{0}".format(self.line) match = DIRECT_URL_RE.match(line) @@ -432,7 +436,8 @@ def parse_extras(self): ref = match_dict.get("ref") subdir = match_dict.get("subdirectory") pathsep = match_dict.get("pathsep", "/") - url = scheme + if scheme is not None: + url = scheme if host: url = "{0}{1}".format(url, host) if path: @@ -452,46 +457,20 @@ def parse_extras(self): self.line = add_ssh_scheme_to_git_uri(url) if name: self._name = name - # line = add_ssh_scheme_to_git_uri(self.line) - # parsed = urllib_parse.urlparse(line) - # if not parsed.scheme and "@" in line: - # matched = URL_RE.match(line) - # if matched is None: - # matched = NAME_RE.match(line) - # if matched: - # name = matched.groupdict().get("name") - # if name is not None: - # self._name = name - # extras = matched.groupdict().get("extras") - # else: - # name, _, line = self.line.partition("@") - # name = name.strip() - # line = line.strip() - # matched = NAME_RE.match(name) - # match_dict = matched.groupdict() - # name = match_dict.get("name") - # extras = match_dict.get("extras") - # if is_vcs(line) or is_valid_url(line): - # self.is_direct_url = True - # # name, extras = pip_shims.shims._strip_extras(name) - # self._name = name - # self.line = line else: self.line, extras = pip_shims.shims._strip_extras(self.line) else: self.line, extras = pip_shims.shims._strip_extras(self.line) + extras_set = set() # type: Set[STRING_TYPE] if extras is not None: - extras = set(parse_extras(extras)) + extras_set = set(parse_extras(extras)) if self._name: self._name, name_extras = pip_shims.shims._strip_extras(self._name) if name_extras: name_extras = set(parse_extras(name_extras)) - if extras: - extras |= name_extras - else: - extras = name_extras - if extras is not None: - self.extras = tuple(sorted(extras)) + extras_set |= name_extras + if extras_set is not None: + self.extras = tuple(sorted(extras_set)) def get_url(self): # type: () -> STRING_TYPE @@ -536,9 +515,9 @@ def name(self, name): self._name = name if self._setup_info: self._setup_info.name = name - if self.requirement: + if self.requirement and self._requirement: self._requirement.name = name - if self.ireq and self.ireq.req: + if self.ireq and self._ireq and self._ireq.req: self._ireq.req.name = name @property @@ -802,7 +781,6 @@ def _parse_name_from_link(self): def _parse_name_from_line(self): # type: () -> Optional[STRING_TYPE] - if not self.is_named: pass try: @@ -819,9 +797,13 @@ def _parse_name_from_line(self): specifier_match = next( iter(spec for spec in SPECIFIERS_BY_LENGTH if spec in self.line), None ) - if specifier_match is not None: - name, specifier_match, version = name.partition(specifier_match) - self._specifier = "{0}{1}".format(specifier_match, version) + specifier = None # type: Optional[STRING_TYPE] + if specifier_match: + specifier = "{0!s}".format(specifier_match) + if specifier is not None and specifier in name: + version = None # type: Optional[STRING_TYPE] + name, specifier, version = name.partition(specifier) + self._specifier = "{0}{1}".format(specifier, version) return name def parse_name(self): @@ -848,10 +830,15 @@ def parse_name(self): def _parse_requirement_from_vcs(self): # type: () -> Optional[PackagingRequirement] + url = self.url if self.url else self.link.url + if url: + url = unquote(url) if ( - self.uri != unquote(self.url) - and "git+ssh://" in self.url + url + and self.uri != url + and "git+ssh://" in url and (self.uri is not None and "git+git@" in self.uri) + and self._requirement is not None ): self._requirement.line = self.uri self._requirement.url = self.url @@ -862,10 +849,10 @@ def _parse_requirement_from_vcs(self): subdirectory=self.subdirectory, extras=self.extras, name=self.name - )) + )) # type: ignore # else: # req.link = self.link - if self.ref: + if self.ref and self._requirement is not None: if self._vcsrepo is not None: self._requirement.revision = self._vcsrepo.get_commit_hash() else: @@ -964,20 +951,20 @@ def parse_markers(self): @property def requirement_info(self): - # type: () -> Tuple[Optional[AnyStr], Tuple[Optional[AnyStr], ...], Optional[AnyStr]] + # type: () -> Tuple[Optional[S], Tuple[Optional[S], ...], Optional[S]] """ Generates a 3-tuple of the requisite *name*, *extras* and *url* to generate a :class:`~packaging.requirements.Requirement` out of. :return: A Tuple containing an optional name, a Tuple of extras names, and an optional URL. - :rtype: Tuple[Optional[AnyStr], Tuple[Optional[AnyStr], ...], Optional[AnyStr]] + :rtype: Tuple[Optional[S], Tuple[Optional[S], ...], Optional[S]] """ # Direct URLs can be converted to packaging requirements directly, but # only if they are `file://` (with only two slashes) - name = None - extras = () - url = None + name = None # type: Optional[S] + extras = () # type: Tuple[Optional[S], ...] + url = None # type: Optional[S] # if self.is_direct_url: if self._name: name = canonicalize_name(self._name) @@ -1000,10 +987,7 @@ def requirement_info(self): self._name = self.link.egg_fragment if self._name: name = canonicalize_name(self._name) - # return "{0}{1}@ {2}".format( - # normalize_name(self.name), extras_to_string(self.extras), url - # ) - return (name, extras, url) + return name, extras, url @property def line_is_installable(self): @@ -1054,7 +1038,7 @@ class NamedRequirement(object): name = attr.ib() # type: STRING_TYPE version = attr.ib() # type: Optional[STRING_TYPE] req = attr.ib() # type: PackagingRequirement - extras = attr.ib(default=attr.Factory(list)) # type: Tuple[STRING_TYPE] + extras = attr.ib(default=attr.Factory(list)) # type: Tuple[STRING_TYPE, ...] editable = attr.ib(default=False) # type: bool _parsed_line = attr.ib(default=None) # type: Optional[Line] @@ -1095,19 +1079,19 @@ def from_line(cls, line, parsed_line=None): "parsed_line": parsed_line, "extras": None } - extras = None # type: Optional[Tuple[STRING_TYPE]] + extras = None # type: Optional[Tuple[STRING_TYPE, ...]] if req.extras: - extras = list(req.extras) + extras = tuple(req.extras) creation_kwargs["extras"] = extras return cls(**creation_kwargs) @classmethod def from_pipfile(cls, name, pipfile): - # type: (AnyStr, Dict[AnyStr, Union[Text, str, List[Union[Text, str]]]]) -> NamedRequirement + # type: (S, Dict[S, Union[S, bool, Union[List[S], Tuple[S, ...], Set[S]]]]) -> NamedRequirement creation_args = {} # type: Dict[STRING_TYPE, Union[Optional[STRING_TYPE], Optional[List[STRING_TYPE]]]] if hasattr(pipfile, "keys"): attr_fields = [field.name for field in attr.fields(cls)] - creation_args = {k: v for k, v in pipfile.items() if k in attr_fields} + creation_args = {k: v for k, v in pipfile.items() if k in attr_fields} # type: ignore creation_args["name"] = name version = get_version(pipfile) # type: Optional[STRING_TYPE] extras = creation_args.get("extras", None) @@ -1155,7 +1139,7 @@ class FileRequirement(object): #: Whether the package is editable editable = attr.ib(default=False, cmp=True) # type: bool #: Extras if applicable - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[STRING_TYPE] + extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Tuple[STRING_TYPE, ...] _uri_scheme = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] #: URI of the package uri = attr.ib(cmp=True) # type: Optional[STRING_TYPE] @@ -1239,7 +1223,7 @@ def get_link_from_line(cls, line): if "+" in original_scheme: scheme = None # type: Optional[STRING_TYPE] vcs_type, _, scheme = original_scheme.partition("+") - parsed_url = parsed_url._replace(scheme=scheme) + parsed_url = parsed_url._replace(scheme=scheme) # type: ignore prefer = "uri" # type: STRING_TYPE else: vcs_type = None @@ -1262,18 +1246,18 @@ def get_link_from_line(cls, line): relpath = None # Cut the fragment, but otherwise this is fixed_line. uri = urllib_parse.urlunsplit( - parsed_url._replace(scheme=original_scheme, fragment="") + parsed_url._replace(scheme=original_scheme, fragment="") # type: ignore ) if added_ssh_scheme: original_uri = urllib_parse.urlunsplit( - original_url._replace(scheme=original_scheme, fragment="") + original_url._replace(scheme=original_scheme, fragment="") # type: ignore ) uri = strip_ssh_from_git_uri(original_uri) # Re-attach VCS prefix to build a Link. link = create_link( - urllib_parse.urlunsplit(parsed_url._replace(scheme=original_scheme)) + urllib_parse.urlunsplit(parsed_url._replace(scheme=original_scheme)) # type: ignore ) return LinkInfo(vcs_type, prefer, relpath, path, uri, link) @@ -1287,10 +1271,10 @@ def setup_py_dir(self): @property def dependencies(self): - # type: () -> Tuple[Dict[AnyStr, PackagingRequirement], List[Union[Text, str, PackagingRequirement]], List[AnyStr]] - build_deps = [] # type: List[Union[Text, str, PackagingRequirement]] - setup_deps = [] # type: List[STRING_TYPE] - deps = {} # type: Dict[STRING_TYPE, PackagingRequirement] + # type: () -> Tuple[Dict[S, PackagingRequirement], List[Union[S, PackagingRequirement]], List[S]] + build_deps = [] # type: List[Union[S, PackagingRequirement]] + setup_deps = [] # type: List[S] + deps = {} # type: Dict[S, PackagingRequirement] if self.setup_info: setup_info = self.setup_info.as_dict() deps.update(setup_info.get("requires", {})) @@ -1309,7 +1293,9 @@ def __attrs_post_init__(self): self._setup_info = self.parsed_line.setup_info if self.parsed_line.setup_info.name: self.name = self.parsed_line.setup_info.name - if self.req is None and self._parsed_line.requirement is not None: + if self.req is None and ( + self._parsed_line is not None and self._parsed_line.requirement is not None + ): self.req = self._parsed_line.requirement if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: if self.req is not None: @@ -1534,7 +1520,7 @@ def create( path=None, # type: Optional[STRING_TYPE] uri=None, # type: STRING_TYPE editable=False, # type: bool - extras=None, # type: Optional[Tuple[STRING_TYPE]] + extras=None, # type: Optional[Tuple[STRING_TYPE, ...]] link=None, # type: Link vcs_type=None, # type: Optional[Any] name=None, # type: Optional[STRING_TYPE] @@ -1551,7 +1537,7 @@ def create( if relpath and not path: path = relpath if not path and uri and link is not None and link.scheme == "file": - path = os.path.abspath(pip_shims.shims.url_to_path(unquote(uri))) + path = os.path.abspath(pip_shims.shims.url_to_path(unquote(uri))) # type: ignore try: path = get_converted_relative_path(path) except ValueError: # Vistir raises a ValueError if it can't make a relpath @@ -1561,7 +1547,7 @@ def create( if not uri_scheme: uri_scheme = "path" if path else "file" if path and not uri: - uri = unquote(pip_shims.shims.path_to_url(os.path.abspath(path))) + uri = unquote(pip_shims.shims.path_to_url(os.path.abspath(path))) # type: ignore if not link: link = cls.get_link_from_line(uri).link if not uri: @@ -1578,17 +1564,18 @@ def create( pyproject_requires = tuple(pyproject_requires) if path: setup_paths = get_setup_paths(path) - if setup_paths["pyproject_toml"] is not None: - pyproject_path = Path(setup_paths["pyproject_toml"]) - if setup_paths["setup_py"] is not None: - setup_path = Path(setup_paths["setup_py"]).as_posix() + if isinstance(setup_paths, Mapping): + if "pyproject_toml" in setup_paths and setup_paths["pyproject_toml"]: + pyproject_path = Path(setup_paths["pyproject_toml"]) + if "setup_py" in setup_paths and setup_paths["setup_py"]: + setup_path = Path(setup_paths["setup_py"]).as_posix() if setup_path and isinstance(setup_path, Path): setup_path = setup_path.as_posix() creation_kwargs = { "editable": editable, "extras": extras, "pyproject_path": pyproject_path, - "setup_path": setup_path if setup_path else None, + "setup_path": setup_path, "uri_scheme": uri_scheme, "link": link, "uri": uri, @@ -1615,8 +1602,8 @@ def create( if name: _line = "{0}#egg={1}".format(_line, name) if extras and extras_to_string(extras) not in _line: - _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) - elif uri is not None: + _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) # type: ignore + elif isinstance(uri, six.string_types): _line = unquote(uri) else: _line = unquote(line) @@ -1625,15 +1612,15 @@ def create( (link and link.scheme == "file") or (uri and uri.startswith("file")) or (not uri and not link) ): - _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) + _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) # type: ignore if ireq is None: - ireq = pip_shims.shims.install_req_from_editable(_line) + ireq = pip_shims.shims.install_req_from_editable(_line) # type: ignore else: _line = path if (uri_scheme and uri_scheme == "path") else _line if extras and extras_to_string(extras) not in _line: - _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) + _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) # type: ignore if ireq is None: - ireq = pip_shims.shims.install_req_from_line(_line) + ireq = pip_shims.shims.install_req_from_line(_line) # type: ignore if editable: _line = "-e {0}".format(editable) parsed_line = Line(_line) @@ -1645,13 +1632,15 @@ def create( setup_info = SetupInfo.from_ireq(ireq) setupinfo_dict = setup_info.as_dict() setup_name = setupinfo_dict.get("name", None) + build_requires = () # type: Tuple[STRING_TYPE, ...] + build_backend = "" if setup_name is not None: name = setup_name - build_requires = setupinfo_dict.get("build_requires", ()) - build_backend = setupinfo_dict.get("build_backend", "") - if not creation_kwargs.get("pyproject_requires") and build_requires: + build_requires = setupinfo_dict.get("build_requires", build_requires) + build_backend = setupinfo_dict.get("build_backend", build_backend) + if "pyproject_requires" not in creation_kwargs and build_requires: creation_kwargs["pyproject_requires"] = tuple(build_requires) - if not creation_kwargs.get("pyproject_backend") and build_backend: + if "pyproject_backend" not in creation_kwargs and build_backend: creation_kwargs["pyproject_backend"] = build_backend if setup_info is None and parsed_line and parsed_line.setup_info: setup_info = parsed_line.setup_info @@ -2187,7 +2176,7 @@ def from_pipfile(cls, name, pipfile): @classmethod def from_line(cls, line, editable=None, extras=None, parsed_line=None): - # type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr]], Optional[Line]) -> VCSRequirement + # type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr, ...]], Optional[Line]) -> VCSRequirement relpath = None if parsed_line is None: parsed_line = Line(line) @@ -2212,12 +2201,14 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): name, extras = pip_shims.shims._strip_extras(link.egg_fragment) else: name, _ = pip_shims.shims._strip_extras(link.egg_fragment) - if extras: - extras = parse_extras(extras) - else: + parsed_extras = None # type: Optional[List[STRING_TYPE]] + extras_tuple = None # type: Optional[Tuple[STRING_TYPE, ...]] + if not extras: line, extras = pip_shims.shims._strip_extras(line) if extras: - extras = tuple(extras) + if isinstance(extras, six.string_types): + parsed_extras = parse_extras(extras) + extras_tuple = tuple(parsed_extras) subdirectory = link.subdirectory_fragment ref = None if uri: @@ -2233,7 +2224,7 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): "name": name if name else parsed_line.name, "path": relpath or path, "editable": editable, - "extras": extras, + "extras": extras_tuple, "link": link, "vcs_type": vcs_type, "line": line, @@ -2265,7 +2256,7 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): @property def line_part(self): - # type: () -> STRING_TYPE + # type: () -> S """requirements.txt compatible line part sans-extras""" if self.is_local: base_link = self.link @@ -2296,12 +2287,18 @@ def line_part(self): @staticmethod def _choose_vcs_source(pipfile): - # type: (Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]]) -> Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]] + # type: (Dict[S, Union[S, Any]]) -> Dict[S, Union[S, Any]] src_keys = [k for k in pipfile.keys() if k in ["path", "uri", "file"]] + vcs_type = "" # type: Optional[STRING_TYPE] + alt_type = "" # type: Optional[STRING_TYPE] + vcs_value = "" # type: STRING_TYPE if src_keys: chosen_key = first(src_keys) vcs_type = pipfile.pop("vcs") - _, pipfile_url = split_vcs_method_from_uri(pipfile.get(chosen_key)) + vcs_value = pipfile[chosen_key] + alt_type, pipfile_url = split_vcs_method_from_uri(vcs_value) + if vcs_type is None: + vcs_type = alt_type pipfile[vcs_type] = pipfile_url for removed in src_keys: pipfile.pop(removed) @@ -2309,7 +2306,7 @@ def _choose_vcs_source(pipfile): @property def pipfile_part(self): - # type: () -> Dict[AnyStr, Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]]] + # type: () -> Dict[S, Dict[S, Union[List[S], S, bool, RequirementType, pip_shims.shims.Link]]] excludes = [ "_repo", "_base_line", "setup_path", "_has_hashed_name", "pyproject_path", "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line", @@ -2340,8 +2337,8 @@ class Requirement(object): _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[STRING_TYPE] index = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] editable = attr.ib(default=None, cmp=True) # type: Optional[bool] - hashes = attr.ib(factory=frozenset, converter=frozenset, cmp=True) # type: Optional[Tuple[STRING_TYPE]] - extras = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[STRING_TYPE]] + hashes = attr.ib(factory=frozenset, converter=frozenset, cmp=True) # type: Optional[Tuple[STRING_TYPE, ...]] + extras = attr.ib(factory=tuple, cmp=True) # type: Optional[Tuple[STRING_TYPE, ...]] abstract_dep = attr.ib(default=None, cmp=False) # type: Optional[AbstractDependency] _line_instance = attr.ib(default=None, cmp=False) # type: Optional[Line] _ireq = attr.ib(default=None, cmp=False) # type: Optional[pip_shims.InstallRequirement] @@ -2377,29 +2374,37 @@ def requirement(self): return None def add_hashes(self, hashes): - # type: (Union[List, Set, Tuple]) -> Requirement + # type: (Union[S, List[S], Set[S], Tuple[S, ...]]) -> Requirement + new_hashes = set() # type: Set[STRING_TYPE] + if self.hashes is not None: + new_hashes |= set(self.hashes) if isinstance(hashes, six.string_types): - new_hashes = set(self.hashes).add(hashes) + new_hashes.add(hashes) else: - new_hashes = set(self.hashes) | set(hashes) - return attr.evolve(self, hashes=frozenset(new_hashes)) + new_hashes |= set(hashes) + return attr.evolve(self, hashes=tuple(new_hashes)) def get_hashes_as_pip(self, as_list=False): - # type: (bool) -> Union[STRING_TYPE, List[STRING_TYPE]] - if self.hashes: - if as_list: - return [HASH_STRING.format(h) for h in self.hashes] - return "".join([HASH_STRING.format(h) for h in self.hashes]) - return "" if not as_list else [] + # type: (bool) -> Union[S, List[S]] + hashes = "" # type: Union[STRING_TYPE, List[STRING_TYPE]] + if as_list: + hashes = [] + if self.hashes: + hashes = [HASH_STRING.format(h) for h in self.hashes] + else: + hashes = "" + if self.hashes: + hashes = "".join([HASH_STRING.format(h) for h in self.hashes]) + return hashes @property def hashes_as_pip(self): - # type: () -> Union[Text, str, List[AnyStr]] + # type: () -> S return self.get_hashes_as_pip() @property def markers_as_pip(self): - # type: () -> STRING_TYPE + # type: () -> S if self.markers: return " ; {0}".format(self.markers).replace('"', "'") @@ -2417,7 +2422,7 @@ def extras_as_pip(self): @cached_property def commit_hash(self): - # type: () -> Optional[STRING_TYPE] + # type: () -> Optional[S] if self.req is None or not isinstance(self.req, VCSRequirement): return None commit_hash = None @@ -2428,7 +2433,7 @@ def commit_hash(self): @_specifiers.default def get_specifiers(self): - # type: () -> STRING_TYPE + # type: () -> S if self.req and self.req.req and self.req.req.specifier: return specs_to_string(self.req.req.specifier) return "" @@ -2464,14 +2469,14 @@ def line_instance(self): include_extras = False if self.is_file_or_url or self.is_vcs or not self._specifiers: include_specifiers = False - line_part = "" + line_part = "" # type: STRING_TYPE if self.req and self.req.line_part: - line_part = self.req.line_part + line_part = "{0!s}".format(self.req.line_part) parts = [] # type: List[STRING_TYPE] parts = [ line_part, self.extras_as_pip if include_extras else "", - self._specifiers if include_specifiers else "", + self._specifiers if include_specifiers and self._specifiers else "", self.markers_as_pip, ] line = "".join(parts) @@ -2574,7 +2579,7 @@ def is_wheel(self): @property def normalized_name(self): - # type: () -> STRING_TYPE + # type: () -> S return canonicalize_name(self.name) def copy(self): @@ -2912,12 +2917,14 @@ def get_abstract_dependencies(self, sources=None): ) def find_all_matches(self, sources=None, finder=None): + # type: (Optional[List[Dict[S, Union[S, bool]]]], Optional[PackageFinder]) -> List[InstallationCandidate] """Find all matching candidates for the current requirement. Consults a finder to find all matching candidates. :param sources: Pipfile-formatted sources, defaults to None :param sources: list[dict], optional + :param PackageFinder finder: A **PackageFinder** instance from pip's repository implementation :return: A list of Installation Candidates :rtype: list[ :class:`~pip._internal.index.InstallationCandidate` ] """ @@ -2951,13 +2958,17 @@ def run_requires(self, sources=None, finder=None): return info_dict def merge_markers(self, markers): + # type: (Union[AnyStr, Marker]) -> None if not isinstance(markers, Marker): markers = Marker(markers) - _markers = set(Marker(self.ireq.markers)) if self.ireq.markers else set(markers) + _markers = set() # type: Set[Marker, ...] + if self.ireq and self.ireq.markers: + _markers.add(Marker(self.ireq.markers)) _markers.add(markers) new_markers = Marker(" or ".join([str(m) for m in sorted(_markers)])) self.markers = str(new_markers) self.req.req.marker = new_markers + return def file_req_from_parsed_line(parsed_line): diff --git a/src/requirementslib/models/setup_info.py b/src/requirementslib/models/setup_info.py index e599f6f7..f278afd2 100644 --- a/src/requirementslib/models/setup_info.py +++ b/src/requirementslib/models/setup_info.py @@ -49,7 +49,7 @@ if MYPY_RUNNING: - from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text, Set + from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text, Set, AnyStr from pip_shims.shims import InstallRequirement, PackageFinder from pkg_resources import ( PathMetadata, DistInfoDistribution, Requirement as PkgResourcesRequirement @@ -58,6 +58,8 @@ TRequirement = TypeVar("TRequirement") RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) MarkerType = TypeVar('MarkerType', covariant=True, bound=Marker) + STRING_TYPE = Union[str, bytes, Text] + S = TypeVar("S", bytes, str, Text) CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv")) @@ -69,7 +71,7 @@ def pep517_subprocess_runner(cmd, cwd=None, extra_environ=None): - # type: (List[Text], Optional[Text], Optional[Dict[Text, Text]]) -> None + # type: (List[AnyStr], Optional[AnyStr], Optional[Dict[AnyStr, AnyStr]]) -> None """The default method of calling the wrapper subprocess.""" env = os.environ.copy() if extra_environ: @@ -135,7 +137,7 @@ def build_pep517(source_dir, build_dir, config_settings=None, dist_type="wheel") @ensure_mkdir_p(mode=0o775) def _get_src_dir(root): - # type: (Text) -> Text + # type: (AnyStr) -> AnyStr src = os.environ.get("PIP_SRC") if src: return src @@ -152,7 +154,7 @@ def _get_src_dir(root): @lru_cache() def ensure_reqs(reqs): - # type: (List[Union[Text, PkgResourcesRequirement]]) -> List[PkgResourcesRequirement] + # type: (List[Union[S, PkgResourcesRequirement]]) -> List[PkgResourcesRequirement] import pkg_resources if not isinstance(reqs, Iterable): raise TypeError("Expecting an Iterable, got %r" % reqs) @@ -168,18 +170,18 @@ def ensure_reqs(reqs): def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, editable=False): - # type: (Optional[InstallRequirement], Optional[Text], Optional[Text], bool) -> Dict[Text, Text] - download_dir = os.path.join(CACHE_DIR, "pkgs") # type: Text + # type: (Optional[InstallRequirement], Optional[AnyStr], Optional[AnyStr], bool) -> Dict[AnyStr, AnyStr] + download_dir = os.path.join(CACHE_DIR, "pkgs") # type: STRING_TYPE mkdir_p(download_dir) - wheel_download_dir = os.path.join(CACHE_DIR, "wheels") # type: Text + wheel_download_dir = os.path.join(CACHE_DIR, "wheels") # type: STRING_TYPE mkdir_p(wheel_download_dir) if src_dir is None: if editable and src_root is not None: src_dir = src_root elif ireq is None and src_root is not None: - src_dir = _get_src_dir(root=src_root) # type: Text + src_dir = _get_src_dir(root=src_root) # type: STRING_TYPE elif ireq is not None and ireq.editable and src_root is not None: src_dir = _get_src_dir(root=src_root) else: @@ -199,7 +201,7 @@ def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, edita def iter_metadata(path, pkg_name=None, metadata_type="egg-info"): - # type: (Text, Optional[Text], Text) -> Generator + # type: (AnyStr, Optional[AnyStr], AnyStr) -> Generator if pkg_name is not None: pkg_variants = get_name_variants(pkg_name) non_matching_dirs = [] @@ -217,7 +219,7 @@ def iter_metadata(path, pkg_name=None, metadata_type="egg-info"): def find_egginfo(target, pkg_name=None): - # type: (Text, Optional[Text]) -> Generator + # type: (AnyStr, Optional[AnyStr]) -> Generator egg_dirs = ( egg_dir for egg_dir in iter_metadata(target, pkg_name=pkg_name) if egg_dir is not None @@ -230,7 +232,7 @@ def find_egginfo(target, pkg_name=None): def find_distinfo(target, pkg_name=None): - # type: (Text, Optional[Text]) -> Generator + # type: (AnyStr, Optional[AnyStr]) -> Generator dist_dirs = ( dist_dir for dist_dir in iter_metadata(target, pkg_name=pkg_name, metadata_type="dist-info") if dist_dir is not None @@ -243,7 +245,7 @@ def find_distinfo(target, pkg_name=None): def get_metadata(path, pkg_name=None, metadata_type=None): - # type: (Text, Optional[Text], Optional[Text]) -> Dict[Text, Union[Text, List[RequirementType], Dict[Text, RequirementType]]] + # type: (S, Optional[S], Optional[S]) -> Dict[S, Union[S, List[RequirementType], Dict[S, RequirementType]]] metadata_dirs = [] wheel_allowed = metadata_type == "wheel" or metadata_type is None egg_allowed = metadata_type == "egg" or metadata_type is None @@ -279,7 +281,7 @@ def get_metadata(path, pkg_name=None, metadata_type=None): @lru_cache() def get_extra_name_from_marker(marker): - # type: (MarkerType) -> Optional[Text] + # type: (MarkerType) -> Optional[S] if not marker: raise ValueError("Invalid value for marker: {0!r}".format(marker)) if not getattr(marker, "_markers", None): @@ -291,7 +293,7 @@ def get_extra_name_from_marker(marker): def get_metadata_from_wheel(wheel_path): - # type: (Text) -> Dict[Any, Any] + # type: (S) -> Dict[Any, Any] if not isinstance(wheel_path, six.string_types): raise TypeError("Expected string instance, received {0!r}".format(wheel_path)) try: @@ -327,7 +329,7 @@ def get_metadata_from_wheel(wheel_path): def get_metadata_from_dist(dist): - # type: (Union[PathMetadata, DistInfoDistribution]) -> Dict[Text, Union[Text, List[RequirementType], Dict[Text, RequirementType]]] + # type: (Union[PathMetadata, DistInfoDistribution]) -> Dict[S, Union[S, List[RequirementType], Dict[S, RequirementType]]] try: requires = dist.requires() except Exception: @@ -366,25 +368,25 @@ def get_metadata_from_dist(dist): @attr.s(slots=True, frozen=True) class BaseRequirement(object): - name = attr.ib(default="", cmp=True) # type: Text + name = attr.ib(default="", cmp=True) # type: STRING_TYPE requirement = attr.ib(default=None, cmp=True) # type: Optional[PkgResourcesRequirement] def __str__(self): - # type: () -> Text + # type: () -> S return "{0}".format(str(self.requirement)) def as_dict(self): - # type: () -> Dict[Text, Optional[PkgResourcesRequirement]] + # type: () -> Dict[S, Optional[PkgResourcesRequirement]] return {self.name: self.requirement} def as_tuple(self): - # type: () -> Tuple[Text, Optional[PkgResourcesRequirement]] + # type: () -> Tuple[S, Optional[PkgResourcesRequirement]] return (self.name, self.requirement) @classmethod @lru_cache() def from_string(cls, line): - # type: (Text) -> BaseRequirement + # type: (S) -> BaseRequirement line = line.strip() req = init_requirement(line) return cls.from_req(req) @@ -406,11 +408,11 @@ def from_req(cls, req): @attr.s(slots=True, frozen=True) class Extra(object): - name = attr.ib(default=None, cmp=True) # type: Text + name = attr.ib(default=None, cmp=True) # type: STRING_TYPE requirements = attr.ib(factory=frozenset, cmp=True, type=frozenset) def __str__(self): - # type: () -> Text + # type: () -> S return "{0}: {{{1}}}".format(self.section, ", ".join([r.name for r in self.requirements])) def add(self, req): @@ -420,18 +422,18 @@ def add(self, req): return self def as_dict(self): - # type: () -> Dict[Text, Tuple[RequirementType, ...]] + # type: () -> Dict[S, Tuple[RequirementType, ...]] return {self.name: tuple([r.requirement for r in self.requirements])} @attr.s(slots=True, cmp=True, hash=True) class SetupInfo(object): - name = attr.ib(default=None, cmp=True) # type: Text - base_dir = attr.ib(default=None, cmp=True, hash=False) # type: Text - version = attr.ib(default=None, cmp=True) # type: Text + name = attr.ib(default=None, cmp=True) # type: STRING_TYPE + base_dir = attr.ib(default=None, cmp=True, hash=False) # type: STRING_TYPE + version = attr.ib(default=None, cmp=True) # type: STRING_TYPE _requirements = attr.ib(type=frozenset, factory=frozenset, cmp=True, hash=True) build_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) - build_backend = attr.ib(cmp=True) # type: Text + build_backend = attr.ib(cmp=True) # type: STRING_TYPE setup_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None, cmp=True) _extras_requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True) @@ -440,20 +442,21 @@ class SetupInfo(object): pyproject = attr.ib(type=Path, default=None, cmp=True, hash=False) ireq = attr.ib(default=None, cmp=True, hash=False) # type: Optional[InstallRequirement] extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict, cmp=False, hash=False) - metadata = attr.ib(default=None) # type: Optional[Tuple[Text]] + metadata = attr.ib(default=None) # type: Optional[Tuple[STRING_TYPE]] @build_backend.default def get_build_backend(self): + # type: () -> S return get_default_pyproject_backend() @property def requires(self): - # type: () -> Dict[Text, RequirementType] + # type: () -> Dict[S, RequirementType] return {req.name: req.requirement for req in self._requirements} @property def extras(self): - # type: () -> Dict[Text, Optional[Any]] + # type: () -> Dict[S, Optional[Any]] extras_dict = {} extras = set(self._extras_requirements) for section, deps in extras: @@ -465,7 +468,7 @@ def extras(self): @classmethod def get_setup_cfg(cls, setup_cfg_path): - # type: (Text) -> Dict[Text, Union[Text, None, Set[BaseRequirement], List[Text], Tuple[Text, Tuple[BaseRequirement]]]] + # type: (S) -> Dict[S, Union[S, None, Set[BaseRequirement], List[S], Tuple[S, Tuple[BaseRequirement]]]] if os.path.exists(setup_cfg_path): default_opts = { "metadata": {"name": "", "version": ""}, @@ -514,7 +517,8 @@ def get_setup_cfg(cls, setup_cfg_path): @property def egg_base(self): - base = None # type: Optional[Text] + # type: () -> S + base = None # type: Optional[STRING_TYPE] if self.setup_py.exists(): base = self.setup_py.parent elif self.pyproject.exists(): @@ -637,7 +641,7 @@ def pep517_config(self): return config def build_wheel(self): - # type: () -> Text + # type: () -> S if not self.pyproject.exists(): build_requires = ", ".join(['"{0}"'.format(r) for r in self.build_requires]) self.pyproject.write_text(u""" @@ -653,7 +657,7 @@ def build_wheel(self): # noinspection PyPackageRequirements def build_sdist(self): - # type: () -> Text + # type: () -> S if not self.pyproject.exists(): build_requires = ", ".join(['"{0}"'.format(r) for r in self.build_requires]) self.pyproject.write_text(u""" @@ -668,7 +672,7 @@ def build_sdist(self): ) def build(self): - # type: () -> Optional[Text] + # type: () -> None dist_path = None try: dist_path = self.build_wheel() @@ -686,9 +690,10 @@ def build(self): self.get_egg_metadata() if not self.metadata or not self.name: self.run_setup() + return None def reload(self): - # type: () -> Dict[Text, Any] + # type: () -> Dict[S, Any] """ Wipe existing distribution info metadata for rebuilding. """ @@ -700,13 +705,13 @@ def reload(self): self.get_info() def get_metadata_from_wheel(self, wheel_path): - # type: (Text) -> Dict[Any, Any] + # type: (S) -> Dict[Any, Any] metadata_dict = get_metadata_from_wheel(wheel_path) if metadata_dict: self.populate_metadata(metadata_dict) def get_egg_metadata(self, metadata_dir=None, metadata_type=None): - # type: (Optional[Text], Optional[Text]) -> None + # type: (Optional[AnyStr], Optional[AnyStr]) -> None package_indicators = [self.pyproject, self.setup_py, self.setup_cfg] # if self.setup_py is not None and self.setup_py.exists(): metadata_dirs = [] @@ -776,7 +781,7 @@ def run_pyproject(self): self.build_requires = ("setuptools", "wheel") def get_info(self): - # type: () -> Dict[Text, Any] + # type: () -> Dict[S, Any] if self.setup_cfg and self.setup_cfg.exists(): with cd(self.base_dir): self.parse_setup_cfg() @@ -800,7 +805,7 @@ def get_info(self): return self.as_dict() def as_dict(self): - # type: () -> Dict[Text, Any] + # type: () -> Dict[S, Any] prop_dict = { "name": self.name, "version": self.version, @@ -829,7 +834,7 @@ def from_requirement(cls, requirement, finder=None): @classmethod @lru_cache() def from_ireq(cls, ireq, subdir=None, finder=None): - # type: (InstallRequirement, Optional[Text], Optional[PackageFinder]) -> Optional[SetupInfo] + # type: (InstallRequirement, Optional[AnyStr], Optional[PackageFinder]) -> Optional[SetupInfo] import pip_shims.shims if not ireq.link: return @@ -891,7 +896,7 @@ def from_ireq(cls, ireq, subdir=None, finder=None): @classmethod def create(cls, base_dir, subdirectory=None, ireq=None, kwargs=None): - # type: (Text, Optional[Text], Optional[InstallRequirement], Optional[Dict[Text, Text]]) -> Optional[SetupInfo] + # type: (AnyStr, Optional[AnyStr], Optional[InstallRequirement], Optional[Dict[AnyStr, AnyStr]]) -> Optional[SetupInfo] if not base_dir or base_dir is None: return diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index 93b1506c..3cc1ec26 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -49,6 +49,7 @@ MarkerTuple = Tuple[TVariable, TOp, TValue] TRequirement = Union[PackagingRequirement, PkgResourcesRequirement] STRING_TYPE = Union[bytes, str, Text] + S = TypeVar("S", bytes, str, Text) HASH_STRING = " --hash={0}" @@ -119,7 +120,7 @@ def init_requirement(name): def extras_to_string(extras): - # type: (Sequence) -> Text + # type: (Sequence) -> S """Turn a list of extras into a string""" if isinstance(extras, six.string_types): if extras.startswith("["): @@ -128,7 +129,7 @@ def extras_to_string(extras): extras = [extras] if not extras: return "" - return "[{0}]".format(",".join(sorted(set(extras)))) + return "[{0}]".format(",".join(sorted(set(extras)))) # type: ignore def parse_extras(extras_str): @@ -160,14 +161,14 @@ def specs_to_string(specs): def build_vcs_uri( - vcs, # type: Optional[STRING_TYPE] - uri, # type: STRING_TYPE - name=None, # type: Optional[STRING_TYPE] - ref=None, # type: Optional[STRING_TYPE] - subdirectory=None, # type: Optional[STRING_TYPE] - extras=None # type: Optional[List[STRING_TYPE]] + vcs, # type: Optional[S] + uri, # type: S + name=None, # type: Optional[S] + ref=None, # type: Optional[S] + subdirectory=None, # type: Optional[S] + extras=None # type: Optional[Union[List[S], Tuple[S, ...]]] ): - # type: (...) -> STRING_TYPE + # type: (...) -> S if extras is None: extras = [] vcs_start = "" @@ -288,7 +289,7 @@ def strip_extras_markers_from_requirement(req): *extra == 'name'*, strip out the extras from the markers and return the cleaned requirement - :param PackagingRequirement req: A pacakaging requirement to clean + :param PackagingRequirement req: A packaging requirement to clean :return: A cleaned requirement :rtype: PackagingRequirement """ @@ -391,7 +392,7 @@ def get_pyproject(path): else: requires = build_system.get("requires", ["setuptools>=40.8", "wheel"]) backend = build_system.get("build-backend", get_default_pyproject_backend()) - return (requires, backend) + return requires, backend def split_markers_from_line(line): @@ -495,8 +496,8 @@ def _requirement_to_str_lowercase_name(requirement): modified to lowercase the dependency name. Previously, we were invoking the original Requirement.__str__ method and - lowercasing the entire result, which would lowercase the name, *and* other, - important stuff that should not be lowercased (such as the marker). See + lower-casing the entire result, which would lowercase the name, *and* other, + important stuff that should not be lower-cased (such as the marker). See this issue for more information: https://github.com/pypa/pipenv/issues/2113. """ From 522a68a115c05f9b01ac448f907639d9bde192ae Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Thu, 28 Feb 2019 02:23:18 -0500 Subject: [PATCH 33/35] More type hints Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 60 +++++++++++++--------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 4959353d..f3926c84 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -25,7 +25,7 @@ from packaging.utils import canonicalize_name from six.moves.urllib import parse as urllib_parse from six.moves.urllib.parse import unquote -from vistir.compat import Path, FileNotFoundError, lru_cache +from vistir.compat import Path, FileNotFoundError, lru_cache, Mapping from vistir.contextmanagers import temp_path from vistir.misc import dedup from vistir.path import ( @@ -347,7 +347,7 @@ def get_requirement_specs(cls, specifierset): @property def requirement(self): - # type: () -> Optional[PackagingRequirement] + # type: () -> Optional[RequirementType] if self._requirement is None: self.parse_requirement() if self._requirement is None and self._name is not None: @@ -964,7 +964,7 @@ def requirement_info(self): # only if they are `file://` (with only two slashes) name = None # type: Optional[S] extras = () # type: Tuple[Optional[S], ...] - url = None # type: Optional[S] + url = None # type: Optional[STRING_TYPE] # if self.is_direct_url: if self._name: name = canonicalize_name(self._name) @@ -1146,7 +1146,7 @@ class FileRequirement(object): #: Link object representing the package to clone link = attr.ib(cmp=True) # type: Optional[Link] #: PyProject Requirements - pyproject_requires = attr.ib(default=attr.Factory(tuple), cmp=True) # type: Optional[Tuple[STRING_TYPE, ...]] + pyproject_requires = attr.ib(factory=tuple, cmp=True) # type: Optional[Tuple[STRING_TYPE, ...]] #: PyProject Build System pyproject_backend = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] #: PyProject Path @@ -1157,7 +1157,7 @@ class FileRequirement(object): _parsed_line = attr.ib(default=None, cmp=False, hash=True) # type: Optional[Line] #: Package name name = attr.ib(cmp=True) # type: Optional[STRING_TYPE] - #: A :class:`~pkg_resources.Requirement` isntance + #: A :class:`~pkg_resources.Requirement` instance req = attr.ib(cmp=True) # type: Optional[PackagingRequirement] @classmethod @@ -1298,7 +1298,7 @@ def __attrs_post_init__(self): ): self.req = self._parsed_line.requirement if self._parsed_line and self._parsed_line.ireq and not self._parsed_line.ireq.req: - if self.req is not None: + if self.req is not None and self._parsed_line._ireq is not None: self._parsed_line._ireq.req = self.req @property @@ -1306,16 +1306,19 @@ def setup_info(self): # type: () -> SetupInfo from .setup_info import SetupInfo if self._setup_info is None and self.parsed_line: - if self.parsed_line.setup_info: - if not self._parsed_line.setup_info.name: + if self.parsed_line and self._parsed_line and self.parsed_line.setup_info: + if self._parsed_line._setup_info and not self._parsed_line._setup_info.name: self._parsed_line._setup_info.get_info() - self._setup_info = self.parsed_line.setup_info - elif self.parsed_line.ireq and not self.parsed_line.is_wheel: + self._setup_info = self.parsed_line._setup_info + elif self.parsed_line and ( + self.parsed_line.ireq and not self.parsed_line.is_wheel + ): self._setup_info = SetupInfo.from_ireq(self.parsed_line.ireq) else: if self.link and not self.link.is_wheel: self._setup_info = Line(self.line_part).setup_info - self._setup_info.get_info() + if self._setup_info: + self._setup_info.get_info() return self._setup_info @setup_info.setter @@ -1343,9 +1346,12 @@ def get_name(self): loc = self.path or self.uri if loc and not self._uri_scheme: self._uri_scheme = "path" if self.path else "file" - name = None - hashed_loc = hashlib.sha256(loc.encode("utf-8")).hexdigest() - hashed_name = hashed_loc[-7:] + name = None # type: Optional[STRING_TYPE] + hashed_loc = None # type: Optional[STRING_TYPE] + hashed_name = None # type: Optional[STRING_TYPE] + if loc: + hashed_loc = hashlib.sha256(loc.encode("utf-8")).hexdigest() + hashed_name = hashed_loc[-7:] if getattr(self, "req", None) and self.req is not None and getattr(self.req, "name") and self.req.name is not None: if self.is_direct_url and self.req.name != hashed_name: return self.req.name @@ -1358,20 +1364,19 @@ def get_name(self): elif self.link and ((self.link.scheme == "file" or self.editable) or ( self.path and self.setup_path and os.path.isfile(str(self.setup_path)) )): - _ireq = None + _ireq = None # type: Optional[InstallRequirement] + target_path = "" # type: STRING_TYPE + if self.setup_py_dir: + target_path = Path(self.setup_py_dir).as_posix() + elif self.path: + target_path = Path(os.path.abspath(self.path)).as_posix() if self.editable: - if self.setup_path: - line = pip_shims.shims.path_to_url(self.setup_py_dir) - else: - line = pip_shims.shims.path_to_url(os.path.abspath(self.path)) + line = pip_shims.shims.path_to_url(target_path) if self.extras: line = "{0}[{1}]".format(line, ",".join(self.extras)) _ireq = pip_shims.shims.install_req_from_editable(line) else: - if self.setup_path: - line = Path(self.setup_py_dir).as_posix() - else: - line = Path(os.path.abspath(self.path)).as_posix() + line = target_path if self.extras: line = "{0}[{1}]".format(line, ",".join(self.extras)) _ireq = pip_shims.shims.install_req_from_line(line) @@ -1435,9 +1440,14 @@ def get_requirement(self): except Exception: pass req = copy.deepcopy(self._parsed_line.requirement) - return req + if req: + return req req = init_requirement(normalize_name(self.name)) + if req is None: + raise ValueError( + "Failed to generate a requirement: missing name for {0!r}".format(self) + ) req.editable = False if self.link is not None: req.line = self.link.url_without_fragment @@ -1542,7 +1552,7 @@ def create( path = get_converted_relative_path(path) except ValueError: # Vistir raises a ValueError if it can't make a relpath path = path - if line and not (uri_scheme and uri and link): + if line is not None and not (uri_scheme and uri and link): vcs_type, uri_scheme, relpath, path, uri, link = cls.get_link_from_line(line) if not uri_scheme: uri_scheme = "path" if path else "file" From a3a458a5f40443d68579ecd4d0ea49f083047995 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 1 Mar 2019 00:12:36 -0500 Subject: [PATCH 34/35] Fix most typehint issues Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 320 ++++++++++++--------- src/requirementslib/models/utils.py | 30 +- 2 files changed, 207 insertions(+), 143 deletions(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index f3926c84..68747bfc 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -77,14 +77,19 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Set, AnyStr, Text + from typing import Optional, TypeVar, List, Dict, Union, Any, Tuple, Set, AnyStr, Text, Generator, FrozenSet from pip_shims.shims import Link, InstallRequirement, PackageFinder, InstallationCandidate RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement) + F = TypeVar("F", "FileRequirement", "VCSRequirement", covariant=True) from six.moves.urllib.parse import SplitResult from .vcs import VCSRepository + from .dependencies import AbstractDependency NON_STRING_ITERABLE = Union[List, Set, Tuple] STRING_TYPE = Union[str, bytes, Text] S = TypeVar("S", bytes, str, Text) + BASE_TYPES = Union[bool, STRING_TYPE, Tuple[STRING_TYPE, ...]] + CUSTOM_TYPES = Union[VCSRepository, RequirementType, SetupInfo, "Line"] + CREATION_ARG_TYPES = Union[BASE_TYPES, Link, CUSTOM_TYPES] SPECIFIERS_BY_LENGTH = sorted(list(Specifier._operators.keys()), key=len, reverse=True) @@ -377,7 +382,7 @@ def populate_setup_paths(self): def pyproject_requires(self): # type: () -> Optional[Tuple[STRING_TYPE, ...]] if self._pyproject_requires is None and self.pyproject_toml is not None: - pyproject_requires, pyproject_backend = get_pyproject(self.path) + pyproject_requires, pyproject_backend = get_pyproject(self.path) # type: ignore if pyproject_requires: self._pyproject_requires = tuple(pyproject_requires) self._pyproject_backend = pyproject_backend @@ -387,7 +392,7 @@ def pyproject_requires(self): def pyproject_backend(self): # type: () -> Optional[STRING_TYPE] if self._pyproject_requires is None and self.pyproject_toml is not None: - pyproject_requires, pyproject_backend = get_pyproject(self.path) + pyproject_requires, pyproject_backend = get_pyproject(self.path) # type: ignore if not pyproject_backend and self.setup_cfg is not None: setup_dict = SetupInfo.get_setup_cfg(self.setup_cfg) pyproject_backend = get_default_pyproject_backend() @@ -842,14 +847,18 @@ def _parse_requirement_from_vcs(self): ): self._requirement.line = self.uri self._requirement.url = self.url - self._requirement.link = create_link(build_vcs_uri( + vcs_uri = build_vcs_uri( # type: ignore vcs=self.vcs, uri=self.url, ref=self.ref, subdirectory=self.subdirectory, extras=self.extras, name=self.name - )) # type: ignore + ) + if vcs_uri: + self._requirement.link = create_link(vcs_uri) + elif self.link: + self._requirement.link = self.link # else: # req.link = self.link if self.ref and self._requirement is not None: @@ -987,7 +996,7 @@ def requirement_info(self): self._name = self.link.egg_fragment if self._name: name = canonicalize_name(self._name) - return name, extras, url + return name, extras, url # type: ignore @property def line_is_installable(self): @@ -1303,7 +1312,7 @@ def __attrs_post_init__(self): @property def setup_info(self): - # type: () -> SetupInfo + # type: () -> Optional[SetupInfo] from .setup_info import SetupInfo if self._setup_info is None and self.parsed_line: if self.parsed_line and self._parsed_line and self.parsed_line.setup_info: @@ -1392,7 +1401,7 @@ def get_name(self): setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir) if setupinfo: self._setup_info = setupinfo - self.setup_info.get_info() + self._setup_info.get_info() setupinfo_dict = setupinfo.as_dict() setup_name = setupinfo_dict.get("name", None) if setup_name: @@ -1541,7 +1550,7 @@ def create( relpath=None, # type: Optional[Any] parsed_line=None, # type: Optional[Line] ): - # type: (...) -> FileRequirement + # type: (...) -> F if parsed_line is None and line is not None: parsed_line = Line(line) if relpath and not path: @@ -1558,20 +1567,20 @@ def create( uri_scheme = "path" if path else "file" if path and not uri: uri = unquote(pip_shims.shims.path_to_url(os.path.abspath(path))) # type: ignore - if not link: + if not link and uri: link = cls.get_link_from_line(uri).link - if not uri: + if not uri and link: uri = unquote(link.url_without_fragment) if not extras: extras = () pyproject_path = None pyproject_requires = None pyproject_backend = None + pyproject_tuple = None # type: Optional[Tuple[STRING_TYPE]] if path is not None: - pyproject_requires = get_pyproject(path) - if pyproject_requires is not None: - pyproject_requires, pyproject_backend = pyproject_requires - pyproject_requires = tuple(pyproject_requires) + pyproject_requires_and_backend = get_pyproject(path) + if pyproject_requires_and_backend is not None: + pyproject_requires, pyproject_backend = pyproject_requires_and_backend if path: setup_paths = get_setup_paths(path) if isinstance(setup_paths, Mapping): @@ -1589,7 +1598,7 @@ def create( "uri_scheme": uri_scheme, "link": link, "uri": uri, - "pyproject_requires": pyproject_requires, + "pyproject_requires": pyproject_tuple, "pyproject_backend": pyproject_backend, "path": path or relpath, "parsed_line": parsed_line @@ -1611,14 +1620,14 @@ def create( _line = unquote(link.url_without_fragment) if name: _line = "{0}#egg={1}".format(_line, name) - if extras and extras_to_string(extras) not in _line: + if _line and extras and extras_to_string(extras) not in _line: _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) # type: ignore elif isinstance(uri, six.string_types): _line = unquote(uri) - else: + elif line: _line = unquote(line) if editable: - if extras and extras_to_string(extras) not in _line and ( + if _line and extras and extras_to_string(extras) not in _line and ( (link and link.scheme == "file") or (uri and uri.startswith("file")) or (not uri and not link) ): @@ -1627,14 +1636,15 @@ def create( ireq = pip_shims.shims.install_req_from_editable(_line) # type: ignore else: _line = path if (uri_scheme and uri_scheme == "path") else _line - if extras and extras_to_string(extras) not in _line: + if _line and extras and extras_to_string(extras) not in _line: _line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras)))) # type: ignore if ireq is None: ireq = pip_shims.shims.install_req_from_line(_line) # type: ignore if editable: _line = "-e {0}".format(editable) - parsed_line = Line(_line) - if ireq is None: + if _line: + parsed_line = Line(_line) + if ireq is None and parsed_line and parsed_line.ireq: ireq = parsed_line.ireq if extras and ireq is not None and not ireq.extras: ireq.extras = set(extras) @@ -1669,12 +1679,11 @@ def create( name = parsed_line.name if name: creation_kwargs["name"] = name - cls_inst = cls(**creation_kwargs) # type: ignore - return cls_inst + return cls(**creation_kwargs) # type: ignore @classmethod - def from_line(cls, line, extras=None, parsed_line=None): - # type: (AnyStr, Optional[Tuple[AnyStr, ...]], Optional[Line]) -> FileRequirement + def from_line(cls, line, editable=None, extras=None, parsed_line=None): + # type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr, ...]], Optional[Line]) -> F line = line.strip('"').strip("'") link = None path = None @@ -1725,7 +1734,7 @@ def from_line(cls, line, extras=None, parsed_line=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (AnyStr, Dict[AnyStr, Any]) -> FileRequirement + # type: (STRING_TYPE, Dict[STRING_TYPE, Union[Tuple[STRING_TYPE, ...], STRING_TYPE, bool]]) -> F # Parse the values out. After this dance we should have two variables: # path - Local filesystem path. # uri - Absolute URI that is parsable with urlsplit. @@ -1733,7 +1742,7 @@ def from_pipfile(cls, name, pipfile): uri = pipfile.get("uri") fil = pipfile.get("file") path = pipfile.get("path") - if path: + if path and isinstance(path, six.string_types): if isinstance(path, Path) and not path.is_absolute(): path = get_converted_relative_path(path.as_posix()) elif not os.path.isabs(path): @@ -1757,43 +1766,57 @@ def from_pipfile(cls, name, pipfile): if not uri: uri = pip_shims.shims.path_to_url(path) - link = cls.get_link_from_line(uri).link + link_info = None # type: Optional[LinkInfo] + if uri and isinstance(uri, six.string_types): + link_info = cls.get_link_from_line(uri) + else: + raise ValueError( + "Failed parsing requirement from pipfile: {0!r}".format(pipfile) + ) + link = None # type: Optional[Link] + if link_info: + link = link_info.link + if link.url_without_fragment: + uri = unquote(link.url_without_fragment) + extras = () # type: Optional[Tuple[STRING_TYPE, ...]] + if "extras" in pipfile: + extras = tuple(pipfile["extras"]) # type: ignore + editable = pipfile["editable"] if "editable" in pipfile else False arg_dict = { "name": name, "path": path, - "uri": unquote(link.url_without_fragment), - "editable": pipfile.get("editable", False), + "uri": uri, + "editable": editable, "link": link, "uri_scheme": uri_scheme, - "extras": pipfile.get("extras", None), + "extras": extras if extras else None, } - extras = pipfile.get("extras", ()) - if extras: - extras = tuple(extras) - line = "" - if pipfile.get("editable", False) and uri_scheme == "path": - line = "{0}".format(path) - if extras: - line = "{0}{1}".format(line, extras_to_string(extras)) + line = "" # type: STRING_TYPE + extras_string = "" if not extras else extras_to_string(extras) + if editable and uri_scheme == "path": + line = "{0}{1}".format(path, extras_string) else: if name: - if extras: - line_name = "{0}{1}".format(name, extras_to_string(extras)) - else: - line_name = "{0}".format(name) + line_name = "{0}{1}".format(name, extras_string) line = "{0}#egg={1}".format(unquote(link.url_without_fragment), line_name) else: - line = unquote(link.url) - if extras: - line = "{0}{1}".format(line, extras_to_string(extras)) + if link: + line = unquote(link.url) + elif uri and isinstance(uri, six.string_types): + line = uri + else: + raise ValueError( + "Failed parsing requirement from pipfile: {0!r}".format(pipfile) + ) + line = "{0}{1}".format(line, extras_string) if "subdirectory" in pipfile: arg_dict["subdirectory"] = pipfile["subdirectory"] line = "{0}&subdirectory={1}".format(line, pipfile["subdirectory"]) - if pipfile.get("editable", False): + if editable: line = "-e {0}".format(line) arg_dict["line"] = line - return cls.create(**arg_dict) + return cls.create(**arg_dict) # type: ignore @property def line_part(self): @@ -1825,7 +1848,7 @@ def pipfile_part(self): "pyproject_requires", "pyproject_backend", "_setup_info", "_parsed_line" ] filter_func = lambda k, v: bool(v) is True and k.name not in excludes # noqa - pipfile_dict = attr.asdict(self, filter=filter_func).copy() + pipfile_dict = attr.asdict(self, filter=filter_func).copy() # type: Dict name = pipfile_dict.pop("name", None) if name is None: if self.name: @@ -1839,6 +1862,7 @@ def pipfile_part(self): # For local paths and remote installable artifacts (zipfiles, etc) collision_keys = {"file", "uri", "path"} collision_order = ["file", "uri", "path"] # type: List[STRING_TYPE] + collisions = [] # type: List[STRING_TYPE] key_match = next(iter(k for k in collision_order if k in pipfile_dict.keys())) if self._uri_scheme: dict_key = self._uri_scheme @@ -1911,6 +1935,15 @@ def __attrs_post_init__(self): new_uri = "{0}{1}".format(vcs_type, new_uri) self.uri = new_uri + @property + def url(self): + # type: () -> STRING_TYPE + if self.link and self.link.url: + return self.link.url + elif self.uri: + return self.uri + raise ValueError("No valid url found for requirement {0!r}".format(self)) + @link.default def get_link(self): # type: () -> pip_shims.shims.Link @@ -1927,19 +1960,20 @@ def get_link(self): @name.default def get_name(self): - # type: () -> Optional[STRING_TYPE] - return ( - self.link.egg_fragment or self.req.name - if getattr(self, "req", None) - else super(VCSRequirement, self).get_name() - ) + # type: () -> STRING_TYPE + if self.link and self.link.egg_fragment: + return self.link.egg_fragment + if self.req and self.req.name: + return self.req.name + return super(VCSRequirement, self).get_name() @property def vcs_uri(self): # type: () -> Optional[STRING_TYPE] uri = self.uri - if not any(uri.startswith("{0}+".format(vcs)) for vcs in VCS_LIST): - uri = "{0}+{1}".format(self.vcs, uri) + if uri and not any(uri.startswith("{0}+".format(vcs)) for vcs in VCS_LIST): + if self.vcs: + uri = "{0}+{1}".format(self.vcs, uri) return uri @property @@ -1967,7 +2001,11 @@ def setup_info(self, setup_info): @req.default def get_requirement(self): # type: () -> PackagingRequirement - name = self.name or self.link.egg_fragment + name = None # type: Optional[STRING_TYPE] + if self.name: + name = self.name + elif self.link and self.link.egg_fragment: + name = self.link.egg_fragment url = None if self.uri: url = self.uri @@ -1985,8 +2023,10 @@ def get_requirement(self): if url is not None: url = add_ssh_scheme_to_git_uri(url) elif self.uri is not None: - url = self.parse_link_from_line(self.uri).link.url_without_fragment - if url.startswith("git+file:/") and not url.startswith("git+file:///"): + link = self.get_link_from_line(self.uri).link + if link: + url = link.url_without_fragment + if url and url.startswith("git+file:/") and not url.startswith("git+file:///"): url = url.replace("git+file:/", "git+file:///") if url: req.url = url @@ -2004,13 +2044,14 @@ def get_requirement(self): req.path = self.path req.link = self.link if ( + self.link and self.link.url_without_fragment and self.uri and self.uri != unquote(self.link.url_without_fragment) and "git+ssh://" in self.link.url and "git+git@" in self.uri ): req.line = self.uri url = self.link.url_without_fragment - if url.startswith("git+file:/") and not url.startswith("git+file:///"): + if url and url.startswith("git+file:/") and not url.startswith("git+file:///"): url = url.replace("git+file:/", "git+file:///") req.url = url return req @@ -2028,7 +2069,7 @@ def repo(self): return self._repo def get_checkout_dir(self, src_dir=None): - # type: (Optional[AnyStr]) -> AnyStr + # type: (Optional[S]) -> STRING_TYPE src_dir = os.environ.get("PIP_SRC", None) if not src_dir else src_dir checkout_dir = None if self.is_local: @@ -2037,21 +2078,25 @@ def get_checkout_dir(self, src_dir=None): path = pip_shims.shims.url_to_path(self.uri) if path and os.path.exists(path): checkout_dir = os.path.abspath(path) - return checkout_dir + return vistir.compat.fs_encode(checkout_dir) if src_dir is not None: - checkout_dir = os.path.join(os.path.abspath(src_dir), self.name) - mkdir_p(src_dir) + checkout_dir = os.path.join( + os.path.abspath(src_dir), self.name + ) + mkdir_p(vistir.compat.fs_encode(src_dir)) return checkout_dir - return os.path.join(create_tracked_tempdir(prefix="requirementslib"), self.name) + return vistir.compat.fs_encode( + os.path.join(create_tracked_tempdir(prefix="requirementslib"), self.name) + ) def get_vcs_repo(self, src_dir=None, checkout_dir=None): - # type: (Optional[AnyStr], Optional[AnyStr]) -> VCSRepository + # type: (Optional[STRING_TYPE], STRING_TYPE) -> VCSRepository from .vcs import VCSRepository if checkout_dir is None: - checkout_dir = self.get_checkout_dir(src_dir=src_dir) + checkout_dir = self.get_checkout_dir(src_dir=vistir.compat.fs_encode(src_dir)) vcsrepo = VCSRepository( - url=self.link.url, + url=self.url, name=self.name, ref=self.ref if self.ref else None, checkout_directory=checkout_dir, @@ -2082,7 +2127,7 @@ def get_commit_hash(self): return hash_ def update_repo(self, src_dir=None, ref=None): - # type: (Optional[AnyStr], Optional[AnyStr]) -> AnyStr + # type: (Optional[STRING_TYPE], Optional[STRING_TYPE]) -> STRING_TYPE if ref: self.ref = ref else: @@ -2092,7 +2137,8 @@ def update_repo(self, src_dir=None, ref=None): if not self.is_local and ref is not None: self.repo.checkout_ref(ref) repo_hash = self.repo.get_commit_hash() - self.req.revision = repo_hash + if self.req: + self.req.revision = repo_hash return repo_hash @contextmanager @@ -2109,7 +2155,7 @@ def locked_vcs_repo(self, src_dir=None): revision = self.req.revision = vcsrepo.get_commit_hash() # Remove potential ref in the end of uri after ref is parsed - if "@" in self.link.show_url and "@" in self.uri: + if self.link and "@" in self.link.show_url and self.uri and "@" in self.uri: uri, ref = split_ref_from_uri(self.uri) checkout = revision if checkout and ref and ref in checkout: @@ -2119,15 +2165,12 @@ def locked_vcs_repo(self, src_dir=None): if self._parsed_line: self._parsed_line.vcsrepo = vcsrepo if self._setup_info: - _old_setup_info = self._setup_info self._setup_info = attr.evolve( self._setup_info, requirements=(), _extras_requirements=(), build_requires=(), setup_requires=(), version=None, metadata=None ) - if self.parsed_line: + if self.parsed_line and self._parsed_line: self._parsed_line.vcsrepo = vcsrepo - # self._parsed_line._specifier = "=={0}".format(self.setup_info.version) - # self._parsed_line.specifiers = self._parsed_line._specifier if self.req: self.req.specifier = SpecifierSet("=={0}".format(self.setup_info.version)) try: @@ -2138,8 +2181,8 @@ def locked_vcs_repo(self, src_dir=None): @classmethod def from_pipfile(cls, name, pipfile): - # type: (AnyStr, Dict[AnyStr, Union[List[AnyStr], AnyStr, bool]]) -> VCSRequirement - creation_args = {} + # type: (STRING_TYPE, Dict[S, Union[Tuple[S, ...], S, bool]]) -> F + creation_args = {} # type: Dict[STRING_TYPE, CREATION_ARG_TYPES] pipfile_keys = [ k for k in ( @@ -2155,38 +2198,42 @@ def from_pipfile(cls, name, pipfile): + VCS_LIST if k in pipfile ] + # extras = None # type: Optional[Tuple[STRING_TYPE, ...]] for key in pipfile_keys: - if key == "extras": - extras = pipfile.get(key, None) - if extras: - pipfile[key] = sorted(dedup([extra.lower() for extra in extras])) - if key in VCS_LIST: - creation_args["vcs"] = key - target = pipfile.get(key) - drive, path = os.path.splitdrive(target) - if ( - not drive - and not os.path.exists(target) - and ( - is_valid_url(target) - or is_file_url(target) - or target.startswith("git@") - ) - ): - creation_args["uri"] = target + if key == "extras" and key in pipfile: + extras = pipfile[key] + if isinstance(extras, (list, tuple)): + pipfile[key] = tuple(sorted({extra.lower() for extra in extras})) else: - creation_args["path"] = target - if os.path.isabs(target): - creation_args["uri"] = pip_shims.shims.path_to_url(target) - else: - creation_args[key] = pipfile.get(key) + pipfile[key] = extras + if key in VCS_LIST and key in pipfile_keys: + creation_args["vcs"] = key + target = pipfile[key] + if isinstance(target, six.string_types): + drive, path = os.path.splitdrive(target) + if ( + not drive + and not os.path.exists(target) + and ( + is_valid_url(target) + or is_file_url(target) + or target.startswith("git@") + ) + ): + creation_args["uri"] = target + else: + creation_args["path"] = target + if os.path.isabs(target): + creation_args["uri"] = pip_shims.shims.path_to_url(target) + elif key in pipfile_keys: + creation_args[key] = pipfile[key] creation_args["name"] = name - cls_inst = cls(**creation_args) + cls_inst = cls(**creation_args) # type: ignore return cls_inst @classmethod def from_line(cls, line, editable=None, extras=None, parsed_line=None): - # type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr, ...]], Optional[Line]) -> VCSRequirement + # type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr, ...]], Optional[Line]) -> F relpath = None if parsed_line is None: parsed_line = Line(line) @@ -2218,7 +2265,8 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): if extras: if isinstance(extras, six.string_types): parsed_extras = parse_extras(extras) - extras_tuple = tuple(parsed_extras) + if parsed_extras: + extras_tuple = tuple(parsed_extras) subdirectory = link.subdirectory_fragment ref = None if uri: @@ -2254,7 +2302,7 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): path=relpath or path, editable=editable, uri=uri, - extras=extras, + extras=extras_tuple if extras_tuple else tuple(), base_line=line, parsed_line=parsed_line ) @@ -2266,21 +2314,25 @@ def from_line(cls, line, editable=None, extras=None, parsed_line=None): @property def line_part(self): - # type: () -> S + # type: () -> STRING_TYPE """requirements.txt compatible line part sans-extras""" + base = "" # type: STRING_TYPE if self.is_local: base_link = self.link if not self.link: base_link = self.get_link() - final_format = ( - "{{0}}#egg={0}".format(base_link.egg_fragment) - if base_link.egg_fragment - else "{0}" - ) + if base_link and base_link.egg_fragment: + final_format = "{{0}}#egg={0}".format(base_link.egg_fragment) + else: + final_format = "{0}" base = final_format.format(self.vcs_uri) - elif self._parsed_line is not None and self._parsed_line.is_direct_url: + elif self._parsed_line is not None and ( + self._parsed_line.is_direct_url and self._parsed_line.line_with_prefix + ): return self._parsed_line.line_with_prefix - elif getattr(self, "_base_line", None): + elif getattr(self, "_base_line", None) and ( + isinstance(self._base_line, six.string_types) + ): base = self._base_line else: base = getattr(self, "link", self.get_link()).url @@ -2305,11 +2357,13 @@ def _choose_vcs_source(pipfile): if src_keys: chosen_key = first(src_keys) vcs_type = pipfile.pop("vcs") - vcs_value = pipfile[chosen_key] - alt_type, pipfile_url = split_vcs_method_from_uri(vcs_value) - if vcs_type is None: - vcs_type = alt_type - pipfile[vcs_type] = pipfile_url + if chosen_key in pipfile: + vcs_value = pipfile[chosen_key] + alt_type, pipfile_url = split_vcs_method_from_uri(vcs_value) + if vcs_type is None: + vcs_type = alt_type + if vcs_type and pipfile_url: + pipfile[vcs_type] = pipfile_url for removed in src_keys: pipfile.pop(removed) return pipfile @@ -2335,7 +2389,7 @@ def pipfile_part(self): if "vcs" in pipfile_dict: pipfile_dict = self._choose_vcs_source(pipfile_dict) name, _ = pip_shims.shims._strip_extras(name) - return {name: pipfile_dict} + return {name: pipfile_dict} # type: ignore @attr.s(cmp=True, hash=True) @@ -2347,8 +2401,8 @@ class Requirement(object): _specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers), cmp=True) # type: Optional[STRING_TYPE] index = attr.ib(default=None, cmp=True) # type: Optional[STRING_TYPE] editable = attr.ib(default=None, cmp=True) # type: Optional[bool] - hashes = attr.ib(factory=frozenset, converter=frozenset, cmp=True) # type: Optional[Tuple[STRING_TYPE, ...]] - extras = attr.ib(factory=tuple, cmp=True) # type: Optional[Tuple[STRING_TYPE, ...]] + hashes = attr.ib(factory=frozenset, converter=frozenset, cmp=True) # type: FrozenSet[STRING_TYPE] + extras = attr.ib(factory=tuple, cmp=True) # type: Tuple[STRING_TYPE, ...] abstract_dep = attr.ib(default=None, cmp=False) # type: Optional[AbstractDependency] _line_instance = attr.ib(default=None, cmp=False) # type: Optional[Line] _ireq = attr.ib(default=None, cmp=False) # type: Optional[pip_shims.InstallRequirement] @@ -2395,7 +2449,7 @@ def add_hashes(self, hashes): return attr.evolve(self, hashes=tuple(new_hashes)) def get_hashes_as_pip(self, as_list=False): - # type: (bool) -> Union[S, List[S]] + # type: (bool) -> Union[STRING_TYPE, List[STRING_TYPE]] hashes = "" # type: Union[STRING_TYPE, List[STRING_TYPE]] if as_list: hashes = [] @@ -2409,8 +2463,10 @@ def get_hashes_as_pip(self, as_list=False): @property def hashes_as_pip(self): - # type: () -> S - return self.get_hashes_as_pip() + # type: () -> STRING_TYPE + hashes = self.get_hashes_as_pip() + assert isinstance(hashes, six.string_types) + return hashes @property def markers_as_pip(self): @@ -2516,7 +2572,9 @@ def specifiers(self): self.req.version ): self._specifiers = self.req.version - elif not self.editable and self.req and not isinstance(self.req, NamedRequirement): + elif not self.editable and self.req and ( + not isinstance(self.req, NamedRequirement) and self.req.setup_info + ): if self.line_instance and self.line_instance.setup_info and self.line_instance.setup_info.version: self._specifiers = "=={0}".format(self.req.setup_info.version) elif not self._specifiers: @@ -2618,6 +2676,7 @@ def from_line(cls, line): req_markers = PackagingRequirement("fakepkg; {0}".format(parsed_line.markers)) if r is not None and r.req is not None: r.req.marker = getattr(req_markers, "marker", None) if req_markers else None + args = {} # type: Dict[STRING_TYPE, CREATION_ARG_TYPES] args = { "name": r.name, "vcs": parsed_line.vcs, @@ -2971,20 +3030,21 @@ def merge_markers(self, markers): # type: (Union[AnyStr, Marker]) -> None if not isinstance(markers, Marker): markers = Marker(markers) - _markers = set() # type: Set[Marker, ...] + _markers = set() # type: Set[Marker] if self.ireq and self.ireq.markers: _markers.add(Marker(self.ireq.markers)) _markers.add(markers) new_markers = Marker(" or ".join([str(m) for m in sorted(_markers)])) self.markers = str(new_markers) - self.req.req.marker = new_markers + if self.req and self.req.req: + self.req.req.marker = new_markers return def file_req_from_parsed_line(parsed_line): # type: (Line) -> FileRequirement path = parsed_line.relpath if parsed_line.relpath else parsed_line.path - pyproject_requires = None # type: Optional[Tuple[AnyStr, ...]] + pyproject_requires = None # type: Optional[Tuple[STRING_TYPE, ...]] if parsed_line.pyproject_requires is not None: pyproject_requires = tuple(parsed_line.pyproject_requires) req_dict = { @@ -3022,7 +3082,7 @@ def vcs_req_from_parsed_line(parsed_line): )) else: link = parsed_line.link - pyproject_requires = () # type: Optional[Tuple[AnyStr, ...]] + pyproject_requires = () # type: Optional[Tuple[STRING_TYPE, ...]] if parsed_line.pyproject_requires is not None: pyproject_requires = tuple(parsed_line.pyproject_requires) vcs_dict = { diff --git a/src/requirementslib/models/utils.py b/src/requirementslib/models/utils.py index 3cc1ec26..000b9557 100644 --- a/src/requirementslib/models/utils.py +++ b/src/requirementslib/models/utils.py @@ -31,7 +31,7 @@ from ..environment import MYPY_RUNNING if MYPY_RUNNING: - from typing import Union, Optional, List, Set, Any, TypeVar, Tuple, Sequence, Dict, Text, AnyStr + from typing import Union, Optional, List, Set, Any, TypeVar, Tuple, Sequence, Dict, Text, AnyStr, Match, Iterable from attr import _ValidatorType from packaging.requirements import Requirement as PackagingRequirement from pkg_resources import Requirement as PkgResourcesRequirement @@ -120,7 +120,7 @@ def init_requirement(name): def extras_to_string(extras): - # type: (Sequence) -> S + # type: (Iterable[S]) -> S """Turn a list of extras into a string""" if isinstance(extras, six.string_types): if extras.startswith("["): @@ -133,7 +133,7 @@ def extras_to_string(extras): def parse_extras(extras_str): - # type: (AnyStr) -> List + # type: (AnyStr) -> List[AnyStr] """ Turn a string of extras into a parsed extras list """ @@ -155,7 +155,7 @@ def specs_to_string(specs): try: extras = ",".join(["".join(spec) for spec in specs]) except TypeError: - extras = ",".join(["".join(spec._spec) for spec in specs]) + extras = ",".join(["".join(spec._spec) for spec in specs]) # type: ignore return extras return "" @@ -166,9 +166,9 @@ def build_vcs_uri( name=None, # type: Optional[S] ref=None, # type: Optional[S] subdirectory=None, # type: Optional[S] - extras=None # type: Optional[Union[List[S], Tuple[S, ...]]] + extras=None # type: Optional[Iterable[S]] ): - # type: (...) -> S + # type: (...) -> STRING_TYPE if extras is None: extras = [] vcs_start = "" @@ -198,16 +198,19 @@ def convert_direct_url_to_url(direct_url): :return: A reformatted URL for use with Link objects and :class:`~pip_shims.shims.InstallRequirement` objects. :rtype: AnyStr """ - direct_match = DIRECT_URL_RE.match(direct_url) + direct_match = DIRECT_URL_RE.match(direct_url) # type: Optional[Match] if direct_match is None: url_match = URL_RE.match(direct_url) if url_match or is_valid_url(direct_url): return direct_url - match_dict = direct_match.groupdict() + match_dict = {} # type: Dict[STRING_TYPE, Union[Tuple[STRING_TYPE, ...], STRING_TYPE]] + if direct_match is not None: + match_dict = direct_match.groupdict() # type: ignore if not match_dict: raise ValueError("Failed converting value to normal URL, is it a direct URL? {0!r}".format(direct_url)) url_segments = [match_dict.get(s) for s in ("scheme", "host", "path", "pathsep")] - url = "".join([s for s in url_segments if s is not None]) + url = "" # type: STRING_TYPE + url = "".join([s for s in url_segments if s is not None]) # type: ignore new_url = build_vcs_uri( None, url, @@ -268,7 +271,7 @@ def convert_url_to_direct_url(url, name=None): def get_version(pipfile_entry): - # type: (Union[STRING_TYPE, Dict[AnyStr, bool, List[AnyStr]]]) -> AnyStr + # type: (Union[STRING_TYPE, Dict[STRING_TYPE, Union[STRING_TYPE, bool, Iterable[STRING_TYPE]]]]) -> STRING_TYPE if str(pipfile_entry) == "{}" or is_star(pipfile_entry): return "" @@ -348,14 +351,14 @@ def get_default_pyproject_backend(): def get_pyproject(path): - # type: (Union[STRING_TYPE, Path]) -> Tuple[List[STRING_TYPE], STRING_TYPE] + # type: (Union[STRING_TYPE, Path]) -> Optional[Tuple[List[STRING_TYPE], STRING_TYPE]] """ Given a base path, look for the corresponding ``pyproject.toml`` file and return its build_requires and build_backend. :param AnyStr path: The root path of the project, should be a directory (will be truncated) :return: A 2 tuple of build requirements and the build backend - :rtype: Tuple[List[AnyStr], AnyStr] + :rtype: Optional[Tuple[List[AnyStr], AnyStr]] """ if not path: @@ -410,9 +413,10 @@ def split_markers_from_line(line): def split_vcs_method_from_uri(uri): - # type: (AnyStr) -> Tuple[Optional[AnyStr], AnyStr] + # type: (AnyStr) -> Tuple[Optional[STRING_TYPE], STRING_TYPE] """Split a vcs+uri formatted uri into (vcs, uri)""" vcs_start = "{0}+" + vcs = None # type: Optional[STRING_TYPE] vcs = first([vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))]) if vcs: vcs, uri = uri.split("+", 1) From c4b8e5c4e55bd8b73461bb172f92c01756318f90 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Fri, 1 Mar 2019 01:00:48 -0500 Subject: [PATCH 35/35] Fix a few bugs from typechecking Signed-off-by: Dan Ryan --- src/requirementslib/models/requirements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requirementslib/models/requirements.py b/src/requirementslib/models/requirements.py index 68747bfc..d6beb085 100644 --- a/src/requirementslib/models/requirements.py +++ b/src/requirementslib/models/requirements.py @@ -2094,7 +2094,7 @@ def get_vcs_repo(self, src_dir=None, checkout_dir=None): from .vcs import VCSRepository if checkout_dir is None: - checkout_dir = self.get_checkout_dir(src_dir=vistir.compat.fs_encode(src_dir)) + checkout_dir = self.get_checkout_dir(src_dir=src_dir) vcsrepo = VCSRepository( url=self.url, name=self.name,