Commit e4e8e3cb by Sébastien Eustace Committed by GitHub

Fix resolution of path, url and VCS dependencies (#2398)

* Fix resolution of path, url and VCS dependencies

* Fix editable installation of Poetry packages
parent 7ee66c0a
import os
import tempfile
from io import open
from subprocess import CalledProcessError
from clikit.api.io import IO
......@@ -181,9 +180,7 @@ class PipInstaller(BaseInstaller):
return name
def install_directory(self, package):
from poetry.masonry.builder import SdistBuilder
from poetry.factory import Factory
from poetry.utils._compat import decode
from poetry.utils.env import NullEnv
from poetry.utils.toml_file import TomlFile
......@@ -210,17 +207,20 @@ class PipInstaller(BaseInstaller):
setup = os.path.join(req, "setup.py")
has_setup = os.path.exists(setup)
if not has_setup and has_poetry and (package.develop or not has_build_system):
if has_poetry and (package.develop or not has_build_system):
# We actually need to rely on creating a temporary setup.py
# file since pip, as of this comment, does not support
# build-system for editable packages
# We also need it for non-PEP-517 packages
builder = SdistBuilder(
from poetry.masonry.builders.editable import EditableBuilder
builder = EditableBuilder(
Factory().create_poetry(pyproject.parent), NullEnv(), NullIO()
)
with open(setup, "w", encoding="utf-8") as f:
f.write(decode(builder.build_setup()))
builder.build()
return
if package.develop:
args.append("-e")
......
......@@ -339,6 +339,16 @@ class VersionSolver:
if dependency.name in self._locked:
return 1
# VCS, URL, File or Directory dependencies
# represent a single version
if (
dependency.is_vcs()
or dependency.is_url()
or dependency.is_file()
or dependency.is_directory()
):
return 1
try:
return len(self._provider.search_for(dependency))
except ValueError:
......
......@@ -79,3 +79,14 @@ class DirectoryDependency(Dependency):
def is_directory(self):
return True
def __str__(self):
if self.is_root:
return self._pretty_name
return "{} ({} {})".format(
self._pretty_name, self._pretty_constraint, self._path
)
def __hash__(self):
return hash((self._name, self._full_path))
......@@ -42,6 +42,10 @@ class FileDependency(Dependency):
)
@property
def base(self):
return self._base
@property
def path(self):
return self._path
......@@ -59,3 +63,14 @@ class FileDependency(Dependency):
h.update(content)
return h.hexdigest()
def __str__(self):
if self.is_root:
return self._pretty_name
return "{} ({} {})".format(
self._pretty_name, self._pretty_constraint, self._path
)
def __hash__(self):
return hash((self._name, self._full_path))
......@@ -409,6 +409,7 @@ class Package(object):
def clone(self): # type: () -> Package
clone = self.__class__(self.pretty_name, self.version)
clone.description = self.description
clone.category = self.category
clone.optional = self.optional
clone.python_versions = self.python_versions
......
......@@ -38,3 +38,9 @@ class URLDependency(Dependency):
def is_url(self): # type: () -> bool
return True
def __str__(self):
return "{} ({} url)".format(self._pretty_name, self._pretty_constraint)
def __hash__(self):
return hash((self._name, self._url))
......@@ -94,3 +94,11 @@ class VCSDependency(Dependency):
def accepts_prereleases(self): # type: () -> bool
return True
def __str__(self):
return "{} ({} {})".format(
self._pretty_name, self._pretty_constraint, self._vcs
)
def __hash__(self):
return hash((self._name, self._vcs, self._branch, self._tag, self._rev))
......@@ -72,6 +72,7 @@ class Provider:
self._search_for = {}
self._is_debugging = self._io.is_debug() or self._io.is_very_verbose()
self._in_progress = False
self._deferred_cache = {}
@property
def pool(self): # type: () -> Pool
......@@ -164,6 +165,9 @@ class Provider:
Basically, we clone the repository in a temporary directory
and get the information we need by checking out the specified reference.
"""
if dependency in self._deferred_cache:
return [self._deferred_cache[dependency]]
package = self.get_package_from_vcs(
dependency.vcs,
dependency.source,
......@@ -178,6 +182,11 @@ class Provider:
package.requires += package.extras[extra]
dependency._constraint = package.version
dependency._pretty_constraint = package.version.text
self._deferred_cache[dependency] = package
return [package]
@classmethod
......@@ -214,7 +223,17 @@ class Provider:
return package
def search_for_file(self, dependency): # type: (FileDependency) -> List[Package]
package = self.get_package_from_file(dependency.full_path)
if dependency in self._deferred_cache:
dependency, _package = self._deferred_cache[dependency]
package = _package.clone()
else:
package = self.get_package_from_file(dependency.full_path)
dependency._constraint = package.version
dependency._pretty_constraint = package.version.text
self._deferred_cache[dependency] = (dependency, package)
if dependency.name != package.name:
# For now, the dependency's name must match the actual package's name
......@@ -224,6 +243,9 @@ class Provider:
)
)
if dependency.base is not None:
package.root_dir = dependency.base
package.source_url = dependency.path.as_posix()
package.files = [
{"file": dependency.path.name, "hash": "sha256:" + dependency.hash()}
......@@ -270,15 +292,25 @@ class Provider:
def search_for_directory(
self, dependency
): # type: (DirectoryDependency) -> List[Package]
package = self.get_package_from_directory(
dependency.full_path, name=dependency.name
)
if dependency in self._deferred_cache:
dependency, _package = self._deferred_cache[dependency]
package = _package.clone()
else:
package = self.get_package_from_directory(
dependency.full_path, name=dependency.name
)
dependency._constraint = package.version
dependency._pretty_constraint = package.version.text
self._deferred_cache[dependency] = (dependency, package)
package.source_url = dependency.path.as_posix()
package.develop = dependency.develop
if dependency.base is not None:
package.root_dir = dependency.base.as_posix()
package.root_dir = dependency.base
for extra in dependency.extras:
if extra in package.extras:
......@@ -434,6 +466,9 @@ class Provider:
return package
def search_for_url(self, dependency): # type: (URLDependency) -> List[Package]
if dependency in self._deferred_cache:
return [self._deferred_cache[dependency]]
package = self.get_package_from_url(dependency.url)
if dependency.name != package.name:
......@@ -451,6 +486,11 @@ class Provider:
package.requires += package.extras[extra]
dependency._constraint = package.version
dependency._pretty_constraint = package.version.text
self._deferred_cache[dependency] = package
return [package]
@classmethod
......@@ -551,6 +591,17 @@ class Provider:
else:
requires = package.requires
# Retrieving constraints for deferred dependencies
for r in requires:
if r.is_directory():
self.search_for_directory(r)
elif r.is_file():
self.search_for_file(r)
elif r.is_vcs():
self.search_for_vcs(r)
elif r.is_url():
self.search_for_url(r)
dependencies = [
r
for r in requires
......@@ -696,15 +747,15 @@ class Provider:
if (package.dependency.is_directory() or package.dependency.is_file()) and (
dep.is_directory() or dep.is_file()
):
if dep.path.as_posix().startswith(package.source_url):
relative = (Path(package.source_url) / dep.path).relative_to(
package.source_url
relative_path = Path(
os.path.relpath(
dep.full_path.as_posix(), package.root_dir.as_posix()
)
else:
relative = Path(package.source_url) / dep.path
)
# TODO: Improve the way we set the correct relative path for dependencies
dep._path = relative
dep._path = relative_path
clean_dependencies.append(dep)
package.requires = clean_dependencies
......
......@@ -8,5 +8,6 @@ license = "MIT"
[tool.poetry.dependencies]
python = "*"
project-with-extras = {path = "../../project_with_extras/"}
project-with-transitive-file-dependencies = {path = "../project_with_transitive_file_dependencies/"}
[tool.poetry.dev-dependencies]
[tool.poetry]
name = "inner-directory-project"
version = "1.2.4"
description = "This is a description"
authors = ["Your Name <you@example.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = "*"
[tool.poetry.dev-dependencies]
......@@ -8,5 +8,6 @@ license = "MIT"
[tool.poetry.dependencies]
python = "*"
demo = {path = "../../distributions/demo-0.1.0-py2.py3-none-any.whl"}
inner-directory-project = {path = "./inner-directory-project"}
[tool.poetry.dev-dependencies]
[[package]]
category = "main"
description = ""
name = "demo"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.1.0"
[package.dependencies]
pendulum = ">=1.4.4"
[package.extras]
bar = ["tomlkit"]
foo = ["cleo"]
[package.source]
reference = ""
type = "file"
url = "../../distributions/demo-0.1.0-py2.py3-none-any.whl"
[[package]]
category = "main"
description = ""
develop = true
name = "inner-directory-project"
optional = false
python-versions = "*"
version = "1.2.4"
[package.source]
reference = ""
type = "directory"
url = "../project_with_transitive_file_dependencies/inner-directory-project"
[[package]]
category = "main"
description = ""
name = "pendulum"
optional = false
python-versions = "*"
version = "1.4.4"
[[package]]
category = "main"
description = ""
develop = true
name = "project-with-extras"
optional = false
......@@ -14,7 +56,7 @@ extras_b = ["cachy (>=0.2.0)"]
[package.source]
reference = ""
type = "directory"
url = "tests/fixtures/directory/project_with_transitive_directory_dependencies/../../project_with_extras"
url = "../project_with_extras"
[[package]]
category = "main"
......@@ -26,17 +68,42 @@ python-versions = "*"
version = "1.2.3"
[package.dependencies]
project-with-extras = "*"
project-with-extras = "1.2.3"
project-with-transitive-file-dependencies = "1.2.3"
[package.source]
reference = ""
type = "directory"
url = "project_with_transitive_directory_dependencies"
[[package]]
category = "main"
description = ""
develop = true
name = "project-with-transitive-file-dependencies"
optional = false
python-versions = "*"
version = "1.2.3"
[package.dependencies]
demo = "0.1.0"
inner-directory-project = "1.2.4"
[package.source]
reference = ""
type = "directory"
url = "tests/fixtures/directory/project_with_transitive_directory_dependencies"
url = "project_with_transitive_file_dependencies"
[metadata]
content-hash = "123456789"
python-versions = "*"
[metadata.files]
demo = [
{file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"},
]
inner-directory-project = []
pendulum = []
project-with-extras = []
project-with-transitive-directory-dependencies = []
project-with-transitive-file-dependencies = []
......@@ -16,7 +16,21 @@ foo = ["cleo"]
[package.source]
reference = ""
type = "file"
url = "tests/fixtures/directory/project_with_transitive_file_dependencies/../../distributions/demo-0.1.0-py2.py3-none-any.whl"
url = "../distributions/demo-0.1.0-py2.py3-none-any.whl"
[[package]]
category = "main"
description = ""
develop = true
name = "inner-directory-project"
optional = false
python-versions = "*"
version = "1.2.4"
[package.source]
reference = ""
type = "directory"
url = "project_with_transitive_file_dependencies/inner-directory-project"
[[package]]
category = "main"
......@@ -36,12 +50,13 @@ python-versions = "*"
version = "1.2.3"
[package.dependencies]
demo = "*"
demo = "0.1.0"
inner-directory-project = "1.2.4"
[package.source]
reference = ""
type = "directory"
url = "tests/fixtures/directory/project_with_transitive_file_dependencies"
url = "project_with_transitive_file_dependencies"
[metadata]
content-hash = "123456789"
......@@ -51,5 +66,6 @@ python-versions = "*"
demo = [
{file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"},
]
inner-directory-project = []
pendulum = []
project-with-transitive-file-dependencies = []
......@@ -716,11 +716,13 @@ def test_run_installs_with_local_poetry_directory_and_extras(
def test_run_installs_with_local_poetry_directory_transitive(
installer, locker, repo, package, tmpdir
):
file_path = (
fixtures_dir / "directory/project_with_transitive_directory_dependencies/"
package.root_dir = fixtures_dir.joinpath("directory")
directory = fixtures_dir.joinpath("directory").joinpath(
"project_with_transitive_directory_dependencies"
)
package.add_dependency(
"project-with-transitive-directory-dependencies", {"path": str(file_path)}
"project-with-transitive-directory-dependencies",
{"path": str(directory.relative_to(fixtures_dir.joinpath("directory")))},
)
repo.add_package(get_package("pendulum", "1.4.4"))
......@@ -732,15 +734,19 @@ def test_run_installs_with_local_poetry_directory_transitive(
assert locker.written_data == expected
assert len(installer.installer.installs) == 2
assert len(installer.installer.installs) == 6
def test_run_installs_with_local_poetry_file_transitive(
installer, locker, repo, package, tmpdir
):
file_path = fixtures_dir / "directory/project_with_transitive_file_dependencies/"
package.root_dir = fixtures_dir.joinpath("directory")
directory = fixtures_dir.joinpath("directory").joinpath(
"project_with_transitive_file_dependencies"
)
package.add_dependency(
"project-with-transitive-file-dependencies", {"path": str(file_path)}
"project-with-transitive-file-dependencies",
{"path": str(directory.relative_to(fixtures_dir.joinpath("directory")))},
)
repo.add_package(get_package("pendulum", "1.4.4"))
......@@ -752,7 +758,7 @@ def test_run_installs_with_local_poetry_file_transitive(
assert locker.written_data == expected
assert len(installer.installer.installs) == 3
assert len(installer.installer.installs) == 4
def test_run_installs_with_local_setuptools_directory(
......
......@@ -204,16 +204,13 @@ def test_search_for_directory_setup_with_base(provider, directory):
"foo": [get_dependency("cleo")],
"bar": [get_dependency("tomlkit")],
}
assert (
package.root_dir
== (
Path(__file__).parent.parent
/ "fixtures"
/ "git"
/ "github.com"
/ "demo"
/ directory
).as_posix()
assert package.root_dir == (
Path(__file__).parent.parent
/ "fixtures"
/ "git"
/ "github.com"
/ "demo"
/ directory
)
......
......@@ -1928,3 +1928,114 @@ def test_solver_properly_propagates_markers(solver, repo, package):
str(ops[0].package.marker)
== 'python_version >= "3.6" and implementation_name != "pypy"'
)
def test_solver_cannot_choose_another_version_for_directory_dependencies(
solver, repo, package
):
pendulum = get_package("pendulum", "2.0.3")
demo = get_package("demo", "0.1.0")
foo = get_package("foo", "1.2.3")
foo.add_dependency("demo", "<0.1.2")
repo.add_package(foo)
repo.add_package(demo)
repo.add_package(pendulum)
path = (
Path(__file__).parent.parent
/ "fixtures"
/ "git"
/ "github.com"
/ "demo"
/ "demo"
).as_posix()
package.add_dependency("demo", {"path": path})
package.add_dependency("foo", "^1.2.3")
# This is not solvable since the demo version is pinned
# via the directory dependency
with pytest.raises(SolverProblemError):
solver.solve()
def test_solver_cannot_choose_another_version_for_file_dependencies(
solver, repo, package
):
pendulum = get_package("pendulum", "2.0.3")
demo = get_package("demo", "0.0.8")
foo = get_package("foo", "1.2.3")
foo.add_dependency("demo", "<0.1.0")
repo.add_package(foo)
repo.add_package(demo)
repo.add_package(pendulum)
path = (
Path(__file__).parent.parent
/ "fixtures"
/ "distributions"
/ "demo-0.1.0-py2.py3-none-any.whl"
).as_posix()
package.add_dependency("demo", {"path": path})
package.add_dependency("foo", "^1.2.3")
# This is not solvable since the demo version is pinned
# via the file dependency
with pytest.raises(SolverProblemError):
solver.solve()
def test_solver_cannot_choose_another_version_for_git_dependencies(
solver, repo, package
):
pendulum = get_package("pendulum", "2.0.3")
demo = get_package("demo", "0.0.8")
foo = get_package("foo", "1.2.3")
foo.add_dependency("demo", "<0.1.0")
repo.add_package(foo)
repo.add_package(demo)
repo.add_package(pendulum)
package.add_dependency("demo", {"git": "https://github.com/demo/demo.git"})
package.add_dependency("foo", "^1.2.3")
# This is not solvable since the demo version is pinned
# via the file dependency
with pytest.raises(SolverProblemError):
solver.solve()
def test_solver_cannot_choose_another_version_for_url_dependencies(
solver, repo, package, http
):
path = (
Path(__file__).parent.parent
/ "fixtures"
/ "distributions"
/ "demo-0.1.0-py2.py3-none-any.whl"
)
http.register_uri(
"GET",
"https://foo.bar/demo-0.1.0-py2.py3-none-any.whl",
body=path.read_bytes(),
streaming=True,
)
pendulum = get_package("pendulum", "2.0.3")
demo = get_package("demo", "0.0.8")
foo = get_package("foo", "1.2.3")
foo.add_dependency("demo", "<0.1.0")
repo.add_package(foo)
repo.add_package(demo)
repo.add_package(pendulum)
package.add_dependency(
"demo", {"url": "https://foo.bar/distributions/demo-0.1.0-py2.py3-none-any.whl"}
)
package.add_dependency("foo", "^1.2.3")
# This is not solvable since the demo version is pinned
# via the git dependency
with pytest.raises(SolverProblemError):
solver.solve()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment