diff --git a/poetry/core/factory.py b/poetry/core/factory.py old mode 100644 new mode 100755 index d663b2cad..df1eabf75 --- a/poetry/core/factory.py +++ b/poetry/core/factory.py @@ -1,9 +1,14 @@ from __future__ import absolute_import from __future__ import unicode_literals +import logging + +from typing import Any from typing import Dict from typing import List from typing import Optional +from typing import Union +from warnings import warn from .json import validate_object from .packages.dependency import Dependency @@ -14,6 +19,9 @@ from .utils._compat import Path +logger = logging.getLogger(__name__) + + class Factory(object): """ Factory class to create various elements needed by Poetry. @@ -71,21 +79,38 @@ def create_poetry(self, cwd=None): # type: (Optional[Path]) -> Poetry if isinstance(constraint, list): for _constraint in constraint: - package.add_dependency(name, _constraint) + package.add_dependency( + self.create_dependency( + name, _constraint, root_dir=package.root_dir + ) + ) continue - package.add_dependency(name, constraint) + package.add_dependency( + self.create_dependency(name, constraint, root_dir=package.root_dir) + ) if "dev-dependencies" in local_config: for name, constraint in local_config["dev-dependencies"].items(): if isinstance(constraint, list): for _constraint in constraint: - package.add_dependency(name, _constraint, category="dev") + package.add_dependency( + self.create_dependency( + name, + _constraint, + category="dev", + root_dir=package.root_dir, + ) + ) continue - package.add_dependency(name, constraint, category="dev") + package.add_dependency( + self.create_dependency( + name, constraint, category="dev", root_dir=package.root_dir + ) + ) extras = local_config.get("extras", {}) for extra_name, requirements in extras.items(): @@ -134,6 +159,154 @@ def create_poetry(self, cwd=None): # type: (Optional[Path]) -> Poetry return Poetry(poetry_file, local_config, package) + def create_dependency( + self, + name, # type: str + constraint, # type: Union[str, Dict[str, Any]] + category="main", # type: str + root_dir=None, # type: Optional[Path] + ): # type: (...) -> Dependency + from .packages.constraints import parse_constraint as parse_generic_constraint + from .packages.directory_dependency import DirectoryDependency + from .packages.file_dependency import FileDependency + from .packages.url_dependency import URLDependency + from .packages.utils.utils import create_nested_marker + from .packages.vcs_dependency import VCSDependency + from .version.markers import AnyMarker + from .version.markers import parse_marker + + if constraint is None: + constraint = "*" + + if isinstance(constraint, dict): + optional = constraint.get("optional", False) + python_versions = constraint.get("python") + platform = constraint.get("platform") + markers = constraint.get("markers") + if "allows-prereleases" in constraint: + message = ( + 'The "{}" dependency specifies ' + 'the "allows-prereleases" property, which is deprecated. ' + 'Use "allow-prereleases" instead.'.format(name) + ) + warn(message, DeprecationWarning) + logger.warning(message) + + allows_prereleases = constraint.get( + "allow-prereleases", constraint.get("allows-prereleases", False) + ) + + if "git" in constraint: + # VCS dependency + dependency = VCSDependency( + name, + "git", + constraint["git"], + branch=constraint.get("branch", None), + tag=constraint.get("tag", None), + rev=constraint.get("rev", None), + category=category, + optional=optional, + develop=constraint.get("develop", True), + extras=constraint.get("extras", []), + ) + elif "file" in constraint: + file_path = Path(constraint["file"]) + + dependency = FileDependency( + name, + file_path, + category=category, + base=root_dir, + extras=constraint.get("extras", []), + ) + elif "path" in constraint: + path = Path(constraint["path"]) + + if root_dir: + is_file = root_dir.joinpath(path).is_file() + else: + is_file = path.is_file() + + if is_file: + dependency = FileDependency( + name, + path, + category=category, + optional=optional, + base=root_dir, + extras=constraint.get("extras", []), + ) + else: + dependency = DirectoryDependency( + name, + path, + category=category, + optional=optional, + base=root_dir, + develop=constraint.get("develop", True), + extras=constraint.get("extras", []), + ) + elif "url" in constraint: + dependency = URLDependency( + name, + constraint["url"], + category=category, + optional=optional, + extras=constraint.get("extras", []), + ) + else: + version = constraint["version"] + + source_info = {} + if "source" in constraint: + source_info = self.get_source_information(constraint["source"]) + + dependency = Dependency( + name, + version, + optional=optional, + category=category, + allows_prereleases=allows_prereleases, + extras=constraint.get("extras", []), + **source_info + ) + + if not markers: + marker = AnyMarker() + if python_versions: + dependency.python_versions = python_versions + marker = marker.intersect( + parse_marker( + create_nested_marker( + "python_version", dependency.python_constraint + ) + ) + ) + + if platform: + marker = marker.intersect( + parse_marker( + create_nested_marker( + "sys_platform", parse_generic_constraint(platform) + ) + ) + ) + else: + marker = parse_marker(markers) + + if not marker.is_any(): + dependency.marker = marker + + dependency.source_name = constraint.get("source") + else: + dependency = Dependency(name, constraint, category=category) + + return dependency + + def get_source_information(self, source_name): # type: (str) -> Dict[str, str] + return {} + @classmethod def validate( cls, config, strict=False diff --git a/poetry/core/packages/__init__.py b/poetry/core/packages/__init__.py old mode 100644 new mode 100755 index 7c1c05f61..c32f17870 --- a/poetry/core/packages/__init__.py +++ b/poetry/core/packages/__init__.py @@ -1,6 +1,7 @@ import os import re +from typing import List from typing import Optional from typing import Union @@ -29,8 +30,8 @@ def _make_file_or_dir_dep( - name, path, base=None -): # type: (str, Path, Optional[Path]) -> Optional[Union[FileDependency, DirectoryDependency]] + name, path, base=None, extras=None +): # type: (str, Path, Optional[Path], Optional[List[str]]) -> Optional[Union[FileDependency, DirectoryDependency]] """ Helper function to create a file or directoru dependency with the given arguments. If path is not a file or directory that exists, `None` is returned. @@ -41,9 +42,9 @@ def _make_file_or_dir_dep( _path = Path(base) / path if _path.is_file(): - return FileDependency(name, path, base=base) + return FileDependency(name, path, base=base, extras=extras) elif _path.is_dir(): - return DirectoryDependency(name, path, base=base) + return DirectoryDependency(name, path, base=base, extras=extras) return None @@ -120,26 +121,30 @@ def dependency_from_pep_508( if link.scheme.startswith("git+"): url = ParsedUrl.parse(link.url) - dep = VCSDependency(name, "git", url.url, rev=url.rev) + dep = VCSDependency(name, "git", url.url, rev=url.rev, extras=req.extras) elif link.scheme == "git": - dep = VCSDependency(name, "git", link.url_without_fragment) + dep = VCSDependency( + name, "git", link.url_without_fragment, extras=req.extras + ) elif link.scheme in ["http", "https"]: dep = URLDependency(name, link.url) elif is_file_uri: # handle RFC 8089 references path = url_to_path(req.url) - dep = _make_file_or_dir_dep(name=name, path=path, base=relative_to) + dep = _make_file_or_dir_dep( + name=name, path=path, base=relative_to, extras=req.extras + ) else: try: # this is a local path not using the file URI scheme dep = _make_file_or_dir_dep( - name=name, path=Path(req.url), base=relative_to + name=name, path=Path(req.url), base=relative_to, extras=req.extras, ) except ValueError: pass if dep is None: - dep = Dependency(name, version or "*") + dep = Dependency(name, version or "*", extras=req.extras) if version: dep._constraint = parse_constraint(version) @@ -149,7 +154,7 @@ def dependency_from_pep_508( else: constraint = "*" - dep = Dependency(name, constraint) + dep = Dependency(name, constraint, extras=req.extras) if "extra" in markers: # If we have extras, the dependency is optional @@ -213,8 +218,4 @@ def dependency_from_pep_508( if req.marker: dep.marker = req.marker - # Extras - for extra in req.extras: - dep.extras.append(extra) - return dep diff --git a/poetry/core/packages/dependency.py b/poetry/core/packages/dependency.py old mode 100644 new mode 100755 index 9a530f9ac..93107c29d --- a/poetry/core/packages/dependency.py +++ b/poetry/core/packages/dependency.py @@ -1,4 +1,7 @@ +from typing import FrozenSet +from typing import List from typing import Optional +from typing import Union import poetry.core.packages @@ -7,7 +10,6 @@ from poetry.core.semver import VersionRange from poetry.core.semver import VersionUnion from poetry.core.semver import parse_constraint -from poetry.core.utils.helpers import canonicalize_name from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import parse_marker @@ -15,21 +17,32 @@ from .constraints.constraint import Constraint from .constraints.multi_constraint import MultiConstraint from .constraints.union_constraint import UnionConstraint +from .package_specification import PackageSpecification from .utils.utils import convert_markers -class Dependency(object): +class Dependency(PackageSpecification): def __init__( self, name, # type: str - constraint, # type: str + constraint, # type: Union[str, VersionConstraint] optional=False, # type: bool category="main", # type: str allows_prereleases=False, # type: bool - source_name=None, # type: Optional[str] + extras=None, # type: Union[List[str], FrozenSet[str]] + source_type=None, # type: Optional[str] + source_url=None, # type: Optional[str] + source_reference=None, # type: Optional[str] + source_resolved_reference=None, # type: Optional[str] ): - self._name = canonicalize_name(name) - self._pretty_name = name + super(Dependency, self).__init__( + name, + source_type=source_type, + source_url=source_url, + source_reference=source_reference, + source_resolved_reference=source_resolved_reference, + features=extras, + ) try: if not isinstance(constraint, VersionConstraint): @@ -49,7 +62,6 @@ def __init__( ) self._allows_prereleases = allows_prereleases - self._source_name = source_name self._python_versions = "*" self._python_constraint = parse_constraint("*") @@ -57,13 +69,18 @@ def __init__( self._transitive_python_constraint = None self._transitive_marker = None - self._extras = [] + if extras is None: + self._extras = frozenset() + else: + self._extras = frozenset(extras) + self._in_extras = [] self._activated = not self._optional self.is_root = False self.marker = AnyMarker() + self.source_name = None @property def name(self): @@ -85,10 +102,6 @@ def pretty_name(self): def category(self): return self._category - @property - def source_name(self): - return self._source_name - @property def python_versions(self): return self._python_versions @@ -141,7 +154,7 @@ def transitive_python_constraint(self): return self._transitive_python_constraint @property - def extras(self): # type: () -> list + def extras(self): # type: () -> FrozenSet[str] return self._extras @property @@ -343,14 +356,15 @@ def with_constraint(self, constraint): optional=self.is_optional(), category=self.category, allows_prereleases=self.allows_prereleases(), + extras=self._extras, + source_type=self._source_type, + source_url=self._source_url, + source_reference=self._source_reference, ) new.is_root = self.is_root new.python_versions = self.python_versions - for extra in self.extras: - new.extras.append(extra) - for in_extra in self.in_extras: new.in_extras.append(in_extra) @@ -360,19 +374,32 @@ def __eq__(self, other): if not isinstance(other, Dependency): return NotImplemented - return self._name == other.name and self._constraint == other.constraint + return ( + self.is_same_package_as(other) + and self._constraint == other.constraint + and self._extras == other.extras + ) def __ne__(self, other): return not self == other def __hash__(self): - return hash((self._name, self._pretty_constraint)) + return ( + super(Dependency, self).__hash__() + ^ hash(self._constraint) + ^ hash(self._extras) + ) def __str__(self): if self.is_root: return self._pretty_name - return "{} ({})".format(self._pretty_name, self._pretty_constraint) + name = self._pretty_name + + if self._features: + name = "{}[{}]".format(name, ",".join(sorted(self._features))) + + return "{} ({})".format(name, self._pretty_constraint) def __repr__(self): return "<{} {}>".format(self.__class__.__name__, str(self)) diff --git a/poetry/core/packages/directory_dependency.py b/poetry/core/packages/directory_dependency.py old mode 100644 new mode 100755 index 2fc521c98..d85add7bb --- a/poetry/core/packages/directory_dependency.py +++ b/poetry/core/packages/directory_dependency.py @@ -1,3 +1,7 @@ +from typing import List +from typing import Set +from typing import Union + from poetry.core.pyproject import PyProjectTOML from poetry.core.utils._compat import Path @@ -13,16 +17,14 @@ def __init__( optional=False, # type: bool base=None, # type: Path develop=True, # type: bool + extras=None, # type: Union[List[str], Set[str]] ): self._path = path - self._base = base - self._full_path = path + self._base = base or Path.cwd() + self._full_path = self._base.joinpath(self._path).resolve() self._develop = develop self._supports_poetry = False - if self._base and not self._path.is_absolute(): - self._full_path = self._base / self._path - if not self._full_path.exists(): raise ValueError("Directory {} does not exist".format(self._path)) @@ -43,7 +45,14 @@ def __init__( ) super(DirectoryDependency, self).__init__( - name, "*", category=category, optional=optional, allows_prereleases=True + name, + "*", + category=category, + optional=optional, + allows_prereleases=True, + source_type="directory", + source_url=self._full_path.as_posix(), + extras=extras, ) @property @@ -52,7 +61,7 @@ def path(self): @property def full_path(self): - return self._full_path.resolve() + return self._full_path @property def base(self): @@ -68,6 +77,30 @@ def supports_poetry(self): def is_directory(self): return True + def with_constraint(self, constraint): + new = DirectoryDependency( + self.pretty_name, + path=self.path, + base=self.base, + optional=self.is_optional(), + category=self.category, + develop=self._develop, + extras=self._extras, + ) + + new._constraint = constraint + new._pretty_constraint = str(constraint) + + new.is_root = self.is_root + new.python_versions = self.python_versions + new.marker = self.marker + new.transitive_marker = self.transitive_marker + + for in_extra in self.in_extras: + new.in_extras.append(in_extra) + + return new + @property def base_pep_508_name(self): # type: () -> str requirement = self.pretty_name diff --git a/poetry/core/packages/file_dependency.py b/poetry/core/packages/file_dependency.py old mode 100644 new mode 100755 index 1d3fed2bc..3b5b25719 --- a/poetry/core/packages/file_dependency.py +++ b/poetry/core/packages/file_dependency.py @@ -1,6 +1,10 @@ import hashlib import io +from typing import List +from typing import Set +from typing import Union + from poetry.core.packages.utils.utils import path_to_url from poetry.core.utils._compat import Path @@ -15,13 +19,11 @@ def __init__( category="main", # type: str optional=False, # type: bool base=None, # type: Path + extras=None, # type: Union[List[str], Set[str]] ): self._path = path - self._base = base - self._full_path = path - - if self._base and not self._path.is_absolute(): - self._full_path = self._base / self._path + self._base = base or Path.cwd() + self._full_path = self._base.joinpath(self._path).resolve() if not self._full_path.exists(): raise ValueError("File {} does not exist".format(self._path)) @@ -30,7 +32,14 @@ def __init__( raise ValueError("{} is a directory, expected a file".format(self._path)) super(FileDependency, self).__init__( - name, "*", category=category, optional=optional, allows_prereleases=True + name, + "*", + category=category, + optional=optional, + allows_prereleases=True, + source_type="file", + source_url=self._full_path.as_posix(), + extras=extras, ) @property @@ -43,7 +52,7 @@ def path(self): @property def full_path(self): - return self._full_path.resolve() + return self._full_path def is_file(self): return True @@ -56,6 +65,29 @@ def hash(self): return h.hexdigest() + def with_constraint(self, constraint): + new = FileDependency( + self.pretty_name, + path=self.path, + base=self.base, + optional=self.is_optional(), + category=self.category, + extras=self._extras, + ) + + new._constraint = constraint + new._pretty_constraint = str(constraint) + + new.is_root = self.is_root + new.python_versions = self.python_versions + new.marker = self.marker + new.transitive_marker = self.transitive_marker + + for in_extra in self.in_extras: + new.in_extras.append(in_extra) + + return new + @property def base_pep_508_name(self): # type: () -> str requirement = self.pretty_name diff --git a/poetry/core/packages/package.py b/poetry/core/packages/package.py old mode 100644 new mode 100755 index 1f8daa74d..85e98bd88 --- a/poetry/core/packages/package.py +++ b/poetry/core/packages/package.py @@ -1,36 +1,26 @@ # -*- coding: utf-8 -*- import copy -import logging import re from contextlib import contextmanager -from typing import Union -from warnings import warn +from typing import List from poetry.core.semver import Version from poetry.core.semver import parse_constraint from poetry.core.spdx import License from poetry.core.spdx import license_by_id -from poetry.core.utils._compat import Path -from poetry.core.utils.helpers import canonicalize_name from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import parse_marker -from .constraints import parse_constraint as parse_generic_constraint from .dependency import Dependency -from .directory_dependency import DirectoryDependency -from .file_dependency import FileDependency -from .url_dependency import URLDependency +from .package_specification import PackageSpecification from .utils.utils import create_nested_marker -from .vcs_dependency import VCSDependency AUTHOR_REGEX = re.compile(r"(?u)^(?P[- .,\w\d'’\"()]+)(?: <(?P.+?)>)?$") -logger = logging.getLogger(__name__) - -class Package(object): +class Package(PackageSpecification): AVAILABLE_PYTHONS = { "2", @@ -44,12 +34,28 @@ class Package(object): "3.9", } - def __init__(self, name, version, pretty_version=None): + def __init__( + self, + name, + version, + pretty_version=None, + source_type=None, + source_url=None, + source_reference=None, + source_resolved_reference=None, + features=None, + ): """ Creates a new in memory package. """ - self._pretty_name = name - self._name = canonicalize_name(name) + super(Package, self).__init__( + name, + source_type=source_type, + source_url=source_url, + source_reference=source_reference, + source_resolved_reference=source_resolved_reference, + features=features, + ) if not isinstance(version, Version): self._version = Version.parse(version) @@ -70,11 +76,6 @@ def __init__(self, name, version, pretty_version=None): self._license = None self.readme = None - self.source_name = "" - self.source_type = "" - self.source_reference = "" - self.source_url = "" - self.requires = [] self.dev_requires = [] self.extras = {} @@ -118,7 +119,7 @@ def unique_name(self): if self.is_root(): return self._name - return self.name + "-" + self._version.text + return self.complete_name + "-" + self._version.text @property def pretty_string(self): @@ -132,11 +133,20 @@ def full_pretty_version(self): if self.source_type not in ["hg", "git"]: return self._pretty_version + if self.source_resolved_reference: + if len(self.source_resolved_reference) == 40: + return "{} {}".format( + self._pretty_version, self.source_resolved_reference[0:7] + ) + # if source reference is a sha1 hash -- truncate if len(self.source_reference) == 40: return "{} {}".format(self._pretty_version, self.source_reference[0:7]) - return "{} {}".format(self._pretty_version, self.source_reference) + return "{} {}".format( + self._pretty_version, + self._source_resolved_reference or self._source_reference, + ) @property def authors(self): # type: () -> list @@ -284,125 +294,9 @@ def is_root(self): return False def add_dependency( - self, - name, # type: str - constraint=None, # type: Union[str, dict, None] - category="main", # type: str - ): # type: (...) -> Dependency - if constraint is None: - constraint = "*" - - if isinstance(constraint, dict): - optional = constraint.get("optional", False) - python_versions = constraint.get("python") - platform = constraint.get("platform") - markers = constraint.get("markers") - if "allows-prereleases" in constraint: - message = ( - 'The "{}" dependency specifies ' - 'the "allows-prereleases" property, which is deprecated. ' - 'Use "allow-prereleases" instead.'.format(name) - ) - warn(message, DeprecationWarning) - logger.warning(message) - - allows_prereleases = constraint.get( - "allow-prereleases", constraint.get("allows-prereleases", False) - ) - - if "git" in constraint: - # VCS dependency - dependency = VCSDependency( - name, - "git", - constraint["git"], - branch=constraint.get("branch", None), - tag=constraint.get("tag", None), - rev=constraint.get("rev", None), - category=category, - optional=optional, - develop=constraint.get("develop", True), - ) - elif "file" in constraint: - file_path = Path(constraint["file"]) - - dependency = FileDependency( - name, file_path, category=category, base=self.root_dir - ) - elif "path" in constraint: - path = Path(constraint["path"]) - - if self.root_dir: - is_file = (self.root_dir / path).is_file() - else: - is_file = path.is_file() - - if is_file: - dependency = FileDependency( - name, - path, - category=category, - optional=optional, - base=self.root_dir, - ) - else: - dependency = DirectoryDependency( - name, - path, - category=category, - optional=optional, - base=self.root_dir, - develop=constraint.get("develop", True), - ) - elif "url" in constraint: - dependency = URLDependency( - name, constraint["url"], category=category, optional=optional - ) - else: - version = constraint["version"] - - dependency = Dependency( - name, - version, - optional=optional, - category=category, - allows_prereleases=allows_prereleases, - source_name=constraint.get("source"), - ) - - if not markers: - marker = AnyMarker() - if python_versions: - dependency.python_versions = python_versions - marker = marker.intersect( - parse_marker( - create_nested_marker( - "python_version", dependency.python_constraint - ) - ) - ) - - if platform: - marker = marker.intersect( - parse_marker( - create_nested_marker( - "sys_platform", parse_generic_constraint(platform) - ) - ) - ) - else: - marker = parse_marker(markers) - - if not marker.is_any(): - dependency.marker = marker - - if "extras" in constraint: - for extra in constraint["extras"]: - dependency.extras.append(extra) - else: - dependency = Dependency(name, constraint, category=category) - - if category == "dev": + self, dependency, + ): # type: (Dependency) -> "Package" + if dependency.category == "dev": self.dev_requires.append(dependency) else: self.requires.append(dependency) @@ -410,14 +304,63 @@ def add_dependency( return dependency def to_dependency(self): - from . import dependency_from_pep_508 - - name = "{} (=={})".format(self._name, self._version) + from poetry.core.utils._compat import Path + + from .dependency import Dependency + from .directory_dependency import DirectoryDependency + from .file_dependency import FileDependency + from .url_dependency import URLDependency + from .vcs_dependency import VCSDependency + + if self.source_type == "directory": + dep = DirectoryDependency( + self._name, + Path(self._source_url), + category=self.category, + optional=self.optional, + base=self.root_dir, + develop=self.develop, + extras=self.features, + ) + elif self.source_type == "file": + dep = FileDependency( + self._name, + Path(self._source_url), + category=self.category, + optional=self.optional, + base=self.root_dir, + extras=self.features, + ) + elif self.source_type == "url": + dep = URLDependency( + self._name, + self._source_url, + category=self.category, + optional=self.optional, + extras=self.features, + ) + elif self.source_type == "git": + dep = VCSDependency( + self._name, + self.source_type, + self.source_url, + rev=self.source_reference, + resolved_rev=self.source_resolved_reference, + category=self.category, + optional=self.optional, + develop=self.develop, + extras=self.features, + ) + else: + dep = Dependency(self._name, self._version, extras=self.features) if not self.marker.is_any(): - name += " ; {}".format(str(self.marker)) + dep.marker = self.marker + + if self._source_type not in ["directory", "file", "url", "git"]: + return dep - return dependency_from_pep_508(name) + return dep.with_constraint(self._version) @contextmanager def with_python_versions(self, python_versions): @@ -429,17 +372,36 @@ def with_python_versions(self, python_versions): self.python_versions = original_python_versions - def clone(self): # type: () -> Package - clone = self.__class__(self.pretty_name, self.version) + def with_features(self, features): # type: (List[str]) -> "Package" + package = self.clone() + + package._features = frozenset(features) + + return package + + def without_features(self): # type: () -> "Package" + return self.with_features([]) + + def clone(self): # type: () -> "Package" + if self.is_root(): + clone = self.__class__(self.pretty_name, self.version) + else: + clone = self.__class__( + self.pretty_name, + self.version, + source_type=self._source_type, + source_url=self._source_url, + source_reference=self._source_reference, + features=list(self.features), + ) + clone.description = self.description clone.category = self.category clone.optional = self.optional clone.python_versions = self.python_versions clone.marker = self.marker clone.extras = self.extras - clone.source_type = self.source_type - clone.source_url = self.source_url - clone.source_reference = self.source_reference + clone.root_dir = self.root_dir for dep in self.requires: clone.requires.append(dep) @@ -450,39 +412,35 @@ def clone(self): # type: () -> Package return clone def __hash__(self): - return hash((self._name, self._version)) + return super(Package, self).__hash__() ^ hash(self._version) def __eq__(self, other): if not isinstance(other, Package): return NotImplemented - if self.source_type in ["file", "directory", "url", "git"]: - if self.source_type != other.source_type: - return False + return self.is_same_package_as(other) and self._version == other.version + + def __str__(self): + return "{} ({})".format(self.complete_name, self.full_pretty_version) - if self.source_url or other.source_url: - if self.source_url != other.source_url: - return False + def __repr__(self): + args = [repr(self._name), repr(self._version.text)] - if self.source_reference or other.source_reference: - # special handling for packages with references - if not self.source_reference or not other.source_reference: - # case: one reference is defined and is non-empty, but other is not - return False + if self._features: + args.append("features={}".format(repr(self._features))) - if not ( - self.source_reference == other.source_reference - or self.source_reference.startswith(other.source_reference) - or other.source_reference.startswith(self.source_reference) - ): - # case: both references defined, but one is not equal to or a short - # representation of the other - return False + if self._source_type: + args.append("source_type={}".format(repr(self._source_type))) + args.append("source_url={}".format(repr(self._source_url))) - return self._name == other.name and self._version == other.version + if self._source_reference: + args.append("source_reference={}".format(repr(self._source_reference))) - def __str__(self): - return self.unique_name + if self._source_resolved_reference: + args.append( + "source_resolved_reference={}".format( + repr(self._source_resolved_reference) + ) + ) - def __repr__(self): - return "".format(self.name, self.full_pretty_version) + return "Package({})".format(", ".join(args)) diff --git a/poetry/core/packages/package_specification.py b/poetry/core/packages/package_specification.py new file mode 100755 index 000000000..86faaa939 --- /dev/null +++ b/poetry/core/packages/package_specification.py @@ -0,0 +1,118 @@ +from typing import FrozenSet +from typing import List +from typing import Optional + +from poetry.core.utils.helpers import canonicalize_name + + +class PackageSpecification(object): + def __init__( + self, + name, + source_type=None, + source_url=None, + source_reference=None, + source_resolved_reference=None, + features=None, + ): # type: (str, Optional[str], Optional[str], Optional[str], Optional[str], Optional[List[str]]) -> None + self._pretty_name = name + self._name = canonicalize_name(name) + self._source_type = source_type + self._source_url = source_url + self._source_reference = source_reference + self._source_resolved_reference = source_resolved_reference + + if not features: + features = [] + + self._features = frozenset(features) + + @property + def name(self): # type: () -> str + return self._name + + @property + def pretty_name(self): # type: () -> str + return self._pretty_name + + @property + def complete_name(self): # type () -> str + name = self._name + + if self._features: + name = "{}[{}]".format(name, ",".join(sorted(self._features))) + + return name + + @property + def source_type(self): # type: () -> Optional[str] + return self._source_type + + @property + def source_url(self): # type: () -> Optional[str] + return self._source_url + + @property + def source_reference(self): # type: () -> Optional[str] + return self._source_reference + + @property + def source_resolved_reference(self): # type: () -> Optional[str] + return self._source_resolved_reference + + @property + def features(self): # type: () -> FrozenSet[str] + return self._features + + def is_same_package_as(self, other): # type: ("PackageSpecification") -> bool + if other.complete_name != self.complete_name: + return False + + if self._source_type: + if self._source_type != other.source_type: + return False + + if self._source_url or other.source_url: + if self._source_url != other.source_url: + return False + + if self._source_reference or other.source_reference: + # special handling for packages with references + if not self._source_reference or not other.source_reference: + # case: one reference is defined and is non-empty, but other is not + return False + + if not ( + self._source_reference == other.source_reference + or self._source_reference.startswith(other.source_reference) + or other.source_reference.startswith(self._source_reference) + ): + # case: both references defined, but one is not equal to or a short + # representation of the other + return False + + if ( + self._source_resolved_reference + and other.source_resolved_reference + and self._source_resolved_reference + != other.source_resolved_reference + ): + return False + + return True + + def __hash__(self): # type: () -> int + if not self._source_type: + return hash(self._name) + + return ( + hash(self._name) + ^ hash(self._source_type) + ^ hash(self._source_url) + ^ hash(self._source_reference) + ^ hash(self._source_resolved_reference) + ^ hash(self._features) + ) + + def __str__(self): # type: () -> str + raise NotImplementedError() diff --git a/poetry/core/packages/url_dependency.py b/poetry/core/packages/url_dependency.py old mode 100644 new mode 100755 index a3e622837..c8b5d0641 --- a/poetry/core/packages/url_dependency.py +++ b/poetry/core/packages/url_dependency.py @@ -1,3 +1,7 @@ +from typing import List +from typing import Set +from typing import Union + from poetry.core.utils._compat import urlparse from .dependency import Dependency @@ -10,6 +14,7 @@ def __init__( url, # type: str category="main", # type: str optional=False, # type: bool + extras=None, # type: Union(List[str], Set[str]) ): self._url = url @@ -18,7 +23,14 @@ def __init__( raise ValueError("{} does not seem like a valid url".format(url)) super(URLDependency, self).__init__( - name, "*", category=category, optional=optional, allows_prereleases=True + name, + "*", + category=category, + optional=optional, + allows_prereleases=True, + source_type="url", + source_url=self._url, + extras=extras, ) @property @@ -39,6 +51,28 @@ def base_pep_508_name(self): # type: () -> str def is_url(self): # type: () -> bool return True + def with_constraint(self, constraint): + new = URLDependency( + self.pretty_name, + url=self._url, + optional=self.is_optional(), + category=self.category, + extras=self._extras, + ) + + new._constraint = constraint + new._pretty_constraint = str(constraint) + + new.is_root = self.is_root + new.python_versions = self.python_versions + new.marker = self.marker + new.transitive_marker = self.transitive_marker + + for in_extra in self.in_extras: + new.in_extras.append(in_extra) + + return new + def __str__(self): return "{} ({} url)".format(self._pretty_name, self._pretty_constraint) diff --git a/poetry/core/packages/vcs_dependency.py b/poetry/core/packages/vcs_dependency.py old mode 100644 new mode 100755 index 5bbdc4842..49ee5fa1e --- a/poetry/core/packages/vcs_dependency.py +++ b/poetry/core/packages/vcs_dependency.py @@ -1,3 +1,7 @@ +from typing import List +from typing import Set +from typing import Union + from poetry.core.vcs import git from .dependency import Dependency @@ -16,9 +20,11 @@ def __init__( branch=None, tag=None, rev=None, + resolved_rev=None, category="main", optional=False, develop=False, + extras=None, # type: Union[List[str], Set[str]] ): self._vcs = vcs self._source = source @@ -33,7 +39,15 @@ def __init__( self._develop = develop super(VCSDependency, self).__init__( - name, "*", category=category, optional=optional, allows_prereleases=True + name, + "*", + category=category, + optional=optional, + allows_prereleases=True, + source_type=self._vcs.lower(), + source_url=self._source, + source_reference=branch or tag or rev, + extras=extras, ) @property @@ -101,10 +115,44 @@ def is_vcs(self): # type: () -> bool def accepts_prereleases(self): # type: () -> bool return True - def __str__(self): - return "{} ({} {})".format( - self._pretty_name, self._pretty_constraint, self._vcs + def with_constraint(self, constraint): + new = VCSDependency( + self.pretty_name, + self._vcs, + self._source, + branch=self._branch, + tag=self._tag, + rev=self._rev, + resolved_rev=self._source_resolved_reference, + optional=self.is_optional(), + category=self.category, + develop=self._develop, + extras=self._extras, ) + new._constraint = constraint + new._pretty_constraint = str(constraint) + + new.is_root = self.is_root + new.python_versions = self.python_versions + new.marker = self.marker + new.transitive_marker = self.transitive_marker + + for in_extra in self.in_extras: + new.in_extras.append(in_extra) + + return new + + def __str__(self): + reference = self._vcs + if self._branch: + reference += " branch {}".format(self._branch) + elif self._tag: + reference += " tag {}".format(self._tag) + elif self._rev: + reference += " rev {}".format(self._rev) + + return "{} ({} {})".format(self._pretty_name, self._constraint, reference) + def __hash__(self): return hash((self._name, self._vcs, self._branch, self._tag, self._rev)) diff --git a/poetry/core/semver/version_range.py b/poetry/core/semver/version_range.py index 593cb85ef..f8e00324e 100644 --- a/poetry/core/semver/version_range.py +++ b/poetry/core/semver/version_range.py @@ -452,4 +452,9 @@ def __repr__(self): return "".format(str(self)) def __hash__(self): - return hash((self.min, self.max, self.include_min, self.include_max)) + return ( + hash(self.min) + ^ hash(self.max) + ^ hash(self.include_min) + ^ hash(self.include_max) + ) diff --git a/poetry/core/semver/version_union.py b/poetry/core/semver/version_union.py index 9d70b48cc..d60fff647 100644 --- a/poetry/core/semver/version_union.py +++ b/poetry/core/semver/version_union.py @@ -247,6 +247,14 @@ def __eq__(self, other): return self._ranges == other.ranges + def __hash__(self): # type: () -> int + h = hash(self._ranges[0]) + + for range in self._ranges[1:]: + h ^= hash(range) + + return h + def __str__(self): from .version_range import VersionRange diff --git a/tests/conftest.py b/tests/conftest.py index fe988539f..44a4ff892 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import pytest import virtualenv +from poetry.core.factory import Factory from poetry.core.utils._compat import Path from tests.testutils import tempfile @@ -92,3 +93,8 @@ def venv(temporary_directory): # type: (Path) -> Path @pytest.fixture def python(venv): # type: (Path) -> str return (venv / "bin" / "python").as_posix() + + +@pytest.fixture() +def f(): # type: () -> Factory + return Factory() diff --git a/tests/packages/test_dependency.py b/tests/packages/test_dependency.py index 10ea294a4..420521cc8 100644 --- a/tests/packages/test_dependency.py +++ b/tests/packages/test_dependency.py @@ -130,3 +130,11 @@ def test_to_pep_508_with_patch_python_version(python_versions, marker): assert expected == dependency.to_pep_508() assert marker == str(dependency.marker) + + +def test_complete_name(): + assert "foo" == Dependency("foo", ">=1.2.3").complete_name + assert ( + "foo[bar,baz]" + == Dependency("foo", ">=1.2.3", extras=["baz", "bar"]).complete_name + ) diff --git a/tests/packages/test_main.py b/tests/packages/test_main.py index 3df41fefd..a6e6e2ea9 100644 --- a/tests/packages/test_main.py +++ b/tests/packages/test_main.py @@ -49,7 +49,7 @@ def test_dependency_from_pep_508_with_python_version(): assert dep.name == "requests" assert str(dep.constraint) == "2.18.0" - assert dep.extras == [] + assert dep.extras == frozenset() assert dep.python_versions == "~2.7 || ~2.6" assert str(dep.marker) == 'python_version == "2.7" or python_version == "2.6"' @@ -60,7 +60,7 @@ def test_dependency_from_pep_508_with_single_python_version(): assert dep.name == "requests" assert str(dep.constraint) == "2.18.0" - assert dep.extras == [] + assert dep.extras == frozenset() assert dep.python_versions == "~2.7" assert str(dep.marker) == 'python_version == "2.7"' @@ -71,7 +71,7 @@ def test_dependency_from_pep_508_with_platform(): assert dep.name == "requests" assert str(dep.constraint) == "2.18.0" - assert dep.extras == [] + assert dep.extras == frozenset() assert dep.python_versions == "*" assert str(dep.marker) == 'sys_platform == "win32" or sys_platform == "darwin"' @@ -133,7 +133,7 @@ def test_dependency_with_extra(): assert str(dep.constraint) == "2.18.0" assert len(dep.extras) == 1 - assert dep.extras[0] == "security" + assert "security" in dep.extras def test_dependency_from_pep_508_with_python_version_union_of_multi(): @@ -146,7 +146,7 @@ def test_dependency_from_pep_508_with_python_version_union_of_multi(): assert dep.name == "requests" assert str(dep.constraint) == "2.18.0" - assert dep.extras == [] + assert dep.extras == frozenset() assert dep.python_versions == ">=2.7 <2.8 || >=3.4 <3.5" assert str(dep.marker) == ( 'python_version >= "2.7" and python_version < "2.8" ' @@ -230,7 +230,7 @@ def test_dependency_from_pep_508_with_python_full_version(): assert dep.name == "requests" assert str(dep.constraint) == "2.18.0" - assert dep.extras == [] + assert dep.extras == frozenset() assert dep.python_versions == ">=2.7 <2.8 || >=3.4 <3.5.4" assert str(dep.marker) == ( 'python_version >= "2.7" and python_version < "2.8" ' diff --git a/tests/packages/test_package.py b/tests/packages/test_package.py index 5ea2a7c80..10f23d581 100644 --- a/tests/packages/test_package.py +++ b/tests/packages/test_package.py @@ -4,6 +4,7 @@ import pytest from poetry.core.packages import Package +from poetry.core.utils._compat import Path def test_package_authors(): @@ -32,38 +33,44 @@ def test_package_authors_invalid(): @pytest.mark.parametrize("category", ["main", "dev"]) -def test_package_add_dependency_vcs_category(category): +def test_package_add_dependency_vcs_category(category, f): package = Package("foo", "0.1.0") dependency = package.add_dependency( - "poetry", - constraint={"git": "https://github.com/python-poetry/poetry.git"}, - category=category, + f.create_dependency( + "poetry", + {"git": "https://github.com/python-poetry/poetry.git"}, + category=category, + ) ) assert dependency.category == category -def test_package_add_dependency_vcs_category_default_main(): +def test_package_add_dependency_vcs_category_default_main(f): package = Package("foo", "0.1.0") dependency = package.add_dependency( - "poetry", constraint={"git": "https://github.com/python-poetry/poetry.git"} + f.create_dependency( + "poetry", {"git": "https://github.com/python-poetry/poetry.git"} + ) ) assert dependency.category == "main" @pytest.mark.parametrize("category", ["main", "dev"]) @pytest.mark.parametrize("optional", [True, False]) -def test_package_url_category_optional(category, optional): +def test_package_url_category_optional(category, optional, f): package = Package("foo", "0.1.0") dependency = package.add_dependency( - "poetry", - constraint={ - "url": "https://github.com/python-poetry/poetry/releases/download/1.0.5/poetry-1.0.5-linux.tar.gz", - "optional": optional, - }, - category=category, + f.create_dependency( + "poetry", + { + "url": "https://github.com/python-poetry/poetry/releases/download/1.0.5/poetry-1.0.5-linux.tar.gz", + "optional": optional, + }, + category=category, + ) ) assert dependency.category == category assert dependency.is_optional() == optional @@ -76,15 +83,9 @@ def test_package_equality_simple(): def test_package_equality_source_type(): - a1 = Package("a", "0.1.0") - a1.source_type = "file" - - a2 = Package(a1.name, a1.version) - a2.source_type = "directory" - - a3 = Package(a1.name, a1.version) - a3.source_type = a1.source_type - + a1 = Package("a", "0.1.0", source_type="file") + a2 = Package(a1.name, a1.version, source_type="directory") + a3 = Package(a1.name, a1.version, source_type=a1.source_type) a4 = Package(a1.name, a1.version) assert a1 == a1 @@ -96,20 +97,14 @@ def test_package_equality_source_type(): def test_package_equality_source_url(): - a1 = Package("a", "0.1.0") - a1.source_type = "file" - a1.source_url = "/some/path" - - a2 = Package(a1.name, a1.version) - a2.source_type = a1.source_type - a2.source_url = "/some/other/path" - - a3 = Package(a1.name, a1.version) - a3.source_type = a1.source_type - a3.source_url = a1.source_url - - a4 = Package(a1.name, a1.version) - a4.source_type = a1.source_type + a1 = Package("a", "0.1.0", source_type="file", source_url="/some/path") + a2 = Package( + a1.name, a1.version, source_type=a1.source_type, source_url="/some/other/path" + ) + a3 = Package( + a1.name, a1.version, source_type=a1.source_type, source_url=a1.source_url + ) + a4 = Package(a1.name, a1.version, source_type=a1.source_type) assert a1 == a1 assert a1 == a3 @@ -120,24 +115,158 @@ def test_package_equality_source_url(): def test_package_equality_source_reference(): - a1 = Package("a", "0.1.0") - a1.source_type = "git" - a1.source_reference = "c01b317af582501c5ba07b23d5bef3fbada2d4ef" + a1 = Package( + "a", + "0.1.0", + source_type="git", + source_url="https://foo.bar", + source_reference="c01b317af582501c5ba07b23d5bef3fbada2d4ef", + ) + a2 = Package( + a1.name, + a1.version, + source_type="git", + source_url="https://foo.bar", + source_reference="a444731cd243cb5cd04e4d5fb81f86e1fecf8a00", + ) + a3 = Package( + a1.name, + a1.version, + source_type="git", + source_url="https://foo.bar", + source_reference="c01b317af582501c5ba07b23d5bef3fbada2d4ef", + ) + a4 = Package(a1.name, a1.version, source_type="git") - a2 = Package(a1.name, a1.version) - a2.source_type = a1.source_type - a2.source_reference = "a444731cd243cb5cd04e4d5fb81f86e1fecf8a00" + assert a1 == a1 + assert a1 == a3 + assert a1 != a2 + assert a2 != a3 + assert a1 != a4 + assert a2 != a4 - a3 = Package(a1.name, a1.version) - a3.source_type = a1.source_type - a3.source_reference = a1.source_reference - a4 = Package(a1.name, a1.version) - a4.source_type = a1.source_type +def test_package_resolved_reference_is_relevant_for_equality_only_if_present_for_both_packages(): + a1 = Package( + "a", + "0.1.0", + source_type="git", + source_url="https://foo.bar", + source_reference="master", + source_resolved_reference="c01b317af582501c5ba07b23d5bef3fbada2d4ef", + ) + a2 = Package( + a1.name, + a1.version, + source_type="git", + source_url="https://foo.bar", + source_reference="master", + source_resolved_reference="a444731cd243cb5cd04e4d5fb81f86e1fecf8a00", + ) + a3 = Package( + a1.name, + a1.version, + source_type="git", + source_url="https://foo.bar", + source_reference="master", + source_resolved_reference="c01b317af582501c5ba07b23d5bef3fbada2d4ef", + ) + a4 = Package( + a1.name, + a1.version, + source_type="git", + source_url="https://foo.bar", + source_reference="master", + ) assert a1 == a1 assert a1 == a3 assert a1 != a2 assert a2 != a3 - assert a1 != a4 - assert a2 != a4 + assert a1 == a4 + assert a2 == a4 + + +def test_complete_name(): + assert "foo" == Package("foo", "1.2.3").complete_name + assert ( + "foo[bar,baz]" == Package("foo", "1.2.3", features=["baz", "bar"]).complete_name + ) + + +def test_to_dependency(): + package = Package("foo", "1.2.3") + dep = package.to_dependency() + + assert "foo" == dep.name + assert package.version == dep.constraint + + +def test_to_dependency_with_features(): + package = Package("foo", "1.2.3", features=["baz", "bar"]) + dep = package.to_dependency() + + assert "foo" == dep.name + assert package.version == dep.constraint + assert frozenset({"bar", "baz"}) == dep.features + + +def test_to_dependency_for_directory(): + path = Path(__file__).parent.parent.joinpath("fixtures/simple_project") + package = Package( + "foo", + "1.2.3", + source_type="directory", + source_url=path.as_posix(), + features=["baz", "bar"], + ) + dep = package.to_dependency() + + assert "foo" == dep.name + assert package.version == dep.constraint + assert frozenset({"bar", "baz"}) == dep.features + assert dep.is_directory() + assert path == dep.path + assert "directory" == dep.source_type + assert path.as_posix() == dep.source_url + + +def test_to_dependency_for_file(): + path = Path(__file__).parent.parent.joinpath( + "fixtures/distributions/demo-0.1.0.tar.gz" + ) + package = Package( + "foo", + "1.2.3", + source_type="file", + source_url=path.as_posix(), + features=["baz", "bar"], + ) + dep = package.to_dependency() + + assert "foo" == dep.name + assert package.version == dep.constraint + assert frozenset({"bar", "baz"}) == dep.features + assert dep.is_file() + assert path == dep.path + assert "file" == dep.source_type + assert path.as_posix() == dep.source_url + + +def test_to_dependency_for_url(): + package = Package( + "foo", + "1.2.3", + source_type="url", + source_url="https://example.com/path.tar.gz", + features=["baz", "bar"], + ) + dep = package.to_dependency() + + assert "foo" == dep.name + assert package.version == dep.constraint + assert frozenset({"bar", "baz"}) == dep.features + assert dep.is_url() + assert "https://example.com/path.tar.gz" == dep.url + assert "url" == dep.source_type + assert "https://example.com/path.tar.gz" == dep.source_url diff --git a/tests/packages/test_vcs_dependency.py b/tests/packages/test_vcs_dependency.py index 94ec591f3..0b0e170f7 100644 --- a/tests/packages/test_vcs_dependency.py +++ b/tests/packages/test_vcs_dependency.py @@ -23,9 +23,8 @@ def test_to_pep_508_ssh(): def test_to_pep_508_with_extras(): dependency = VCSDependency( - "poetry", "git", "https://github.com/python-poetry/poetry.git" + "poetry", "git", "https://github.com/python-poetry/poetry.git", extras=["foo"] ) - dependency.extras.append("foo") expected = "poetry[foo] @ git+https://github.com/python-poetry/poetry.git@master" @@ -42,10 +41,9 @@ def test_to_pep_508_in_extras(): assert expected == dependency.to_pep_508() dependency = VCSDependency( - "poetry", "git", "https://github.com/python-poetry/poetry.git" + "poetry", "git", "https://github.com/python-poetry/poetry.git", extras=["bar"] ) dependency.in_extras.append("foo") - dependency.extras.append("bar") expected = 'poetry[bar] @ git+https://github.com/python-poetry/poetry.git@master ; extra == "foo"' diff --git a/tests/test_factory.py b/tests/test_factory.py index b5100862e..e3e99d1b7 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -65,7 +65,7 @@ def test_create_poetry(): assert not requests.is_vcs() assert not requests.allows_prereleases() assert requests.is_optional() - assert requests.extras == ["security"] + assert requests.extras == frozenset({"security"}) pathlib2 = dependencies["pathlib2"] assert pathlib2.pretty_constraint == "^2.2"