Commit 5d69edd7 by Sébastien Eustace

Fix handling of duplicate dependencies with same constraint

parent 906f8360
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
- Fixed `self:update` not picking up new versions. - Fixed `self:update` not picking up new versions.
- Fixed a `RuntimeError` on Python 3.7. - Fixed a `RuntimeError` on Python 3.7.
- Fixed bad version number being picked with private repositories. - Fixed bad version number being picked with private repositories.
- Fixed handling of duplicate dependencies with same constraint.
## [0.10.2] - 2018-05-31 ## [0.10.2] - 2018-05-31
......
...@@ -374,6 +374,9 @@ class VersionSolver: ...@@ -374,6 +374,9 @@ class VersionSolver:
return dependency.name return dependency.name
if not version.is_root():
version = self._provider.complete_package(version)
conflict = False conflict = False
for incompatibility in self._provider.incompatibilities_for(version): for incompatibility in self._provider.incompatibilities_for(version):
self._add_incompatibility(incompatibility) self._add_incompatibility(incompatibility)
......
...@@ -264,12 +264,10 @@ class Provider: ...@@ -264,12 +264,10 @@ class Provider:
won't return incompatibilities that have already been returned by a won't return incompatibilities that have already been returned by a
previous call to _incompatibilities_for(). previous call to _incompatibilities_for().
""" """
if package.source_type in ["git", "file", "directory"]: if package.is_root():
dependencies = package.requires
elif package.is_root():
dependencies = package.all_requires dependencies = package.all_requires
else: else:
dependencies = self._dependencies_for(package) dependencies = package.requires
if not self._package.python_constraint.allows_any(package.python_constraint): if not self._package.python_constraint.allows_any(package.python_constraint):
return [ return [
...@@ -295,41 +293,107 @@ class Provider: ...@@ -295,41 +293,107 @@ class Provider:
for dep in dependencies for dep in dependencies
] ]
def dependencies_for( def complete_package(self, package): # type: (str, Version) -> Package
self, package if package.is_root() or package.source_type in {"git", "file", "directory"}:
): # type: (Package) -> Union[List[Dependency], Dependencies] return package
if package.source_type in ["git", "file", "directory"]:
# Information should already be set
return [
r
for r in package.requires
if not r.is_activated() and r.name not in self.UNSAFE_PACKAGES
]
else:
return Dependencies(package, self)
def _dependencies_for(self, package): # type: (Package) -> List[Dependency] package = self._pool.package(
complete_package = self._pool.package(
package.name, package.version.text, extras=package.requires_extras package.name, package.version.text, extras=package.requires_extras
) )
# Update package with new information dependencies = [
package.requires = complete_package.requires
package.description = complete_package.description
package.python_versions = complete_package.python_versions
package.platform = complete_package.platform
package.hashes = complete_package.hashes
package.extras = complete_package.extras
return [
r r
for r in package.requires for r in package.requires
if r.is_activated() if r.is_activated()
and self._package.python_constraint.allows_any(r.python_constraint) and self._package.python_constraint.allows_any(r.python_constraint)
and self._package.platform_constraint.matches(package.platform_constraint) and self._package.platform_constraint.matches(package.platform_constraint)
and r.name not in self.UNSAFE_PACKAGES
] ]
# Searching for duplicate dependencies
#
# If the duplicate dependencies have the same constraint,
# the requirements will be merged.
#
# For instance:
# - enum34; python_version=="2.7"
# - enum34; python_version=="3.3"
#
# will become:
# - enum34; python_version=="2.7" or python_version=="3.3"
#
# TODO: If the duplicate dependencies have different constraints
# we should notify the resolver in some way to make it split the
# current graph.
#
# An example of this is:
# - pypiwin32 (220); sys_platform == "win32" and python_version >= "3.6"
# - pypiwin32 (219); sys_platform == "win32" and python_version < "3.6"
if not package.is_root():
duplicates = {}
for dep in dependencies:
if dep.name not in duplicates:
duplicates[dep.name] = []
duplicates[dep.name].append(dep)
dependencies = []
for dep_name, deps in duplicates.items():
if len(deps) == 1:
dependencies.append(deps[0])
continue
# Regrouping by constraint
by_constraint = {}
for dep in deps:
if dep.constraint not in by_constraint:
by_constraint[dep.constraint] = []
by_constraint[dep.constraint].append(dep)
# We merge by constraint
for constraint, _deps in by_constraint.items():
new_markers = []
for dep in _deps:
pep_508_dep = dep.to_pep_508()
if ";" not in pep_508_dep:
continue
markers = pep_508_dep.split(";")[1].strip()
new_markers.append("({})".format(markers))
if not new_markers:
dependencies += _deps
continue
dep = _deps[0]
new_requirement = "{}; {}".format(
dep.to_pep_508().split(";")[0], " or ".join(new_markers)
)
new_dep = dependency_from_pep_508(new_requirement)
if dep.is_optional() and not dep.is_activated():
new_dep.deactivate()
else:
new_dep.activate()
by_constraint[constraint] = [new_dep]
continue
if len(by_constraint) == 1:
dependencies.append(list(by_constraint.values())[0][0])
continue
# At this point, we have one dependency by constraint
# So we add them to the dependency set
for constraint, _deps in by_constraint.items():
_dep = _deps[0]
dependencies.append(_dep)
package.requires = dependencies
return package
# UI # UI
@property @property
......
...@@ -39,9 +39,9 @@ class Solver: ...@@ -39,9 +39,9 @@ class Solver:
packages = result.packages packages = result.packages
requested = self._package.all_requires requested = self._package.all_requires
graph = self._build_graph(self._package, packages)
for package in packages: for package in packages:
graph = self._build_graph(self._package, packages)
category, optional, python, platform = self._get_tags_for_package( category, optional, python, platform = self._get_tags_for_package(
package, graph package, graph
) )
...@@ -163,10 +163,28 @@ class Solver: ...@@ -163,10 +163,28 @@ class Solver:
for pkg in packages: for pkg in packages:
if pkg.name == dependency.name: if pkg.name == dependency.name:
graph["children"].append( # If there is already a child with this name
self._build_graph(pkg, packages, dependency, dep or dependency) # we merge the requirements
existing = None
for child in graph["children"]:
if child["name"] == pkg.name:
existing = child
continue
child_graph = self._build_graph(
pkg, packages, dependency, dep or dependency
) )
if existing:
existing["python_version"] = str(
parse_constraint(existing["python_version"]).union(
parse_constraint(child_graph["python_version"])
)
)
continue
graph["children"].append(child_graph)
return graph return graph
def _get_tags_for_package(self, package, graph): def _get_tags_for_package(self, package, graph):
......
...@@ -81,6 +81,15 @@ funcsigs = "*" ...@@ -81,6 +81,15 @@ funcsigs = "*"
colorama = "*" colorama = "*"
[[package]] [[package]]
name = "setuptools"
version = "39.2.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
platform = "*"
[[package]]
name = "six" name = "six"
version = "1.11.0" version = "1.11.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
...@@ -102,4 +111,5 @@ more-itertools = [ "11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2 ...@@ -102,4 +111,5 @@ more-itertools = [ "11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2
pluggy = [ "7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",] pluggy = [ "7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",]
py = [ "983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a", "29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",] py = [ "983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a", "29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",]
pytest = [ "6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", "fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1",] pytest = [ "6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", "fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1",]
setuptools = [ "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926", "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2",]
six = [ "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", "70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",] six = [ "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", "70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",]
...@@ -759,3 +759,29 @@ def test_solver_circular_dependency(solver, repo, package): ...@@ -759,3 +759,29 @@ def test_solver_circular_dependency(solver, repo, package):
{"job": "install", "package": package_a}, {"job": "install", "package": package_a},
], ],
) )
def test_solver_duplicate_dependencies_same_constraint(solver, repo, package):
package.add_dependency("A")
package_a = get_package("A", "1.0")
package_a.add_dependency("B", {"version": "^1.0", "python": "2.7"})
package_a.add_dependency("B", {"version": "^1.0", "python": ">=3.4"})
package_b = get_package("B", "1.0")
repo.add_package(package_a)
repo.add_package(package_b)
ops = solver.solve()
check_solver_result(
ops,
[
{"job": "install", "package": package_b},
{"job": "install", "package": package_a},
],
)
op = ops[0]
assert op.package.requirements == {"python": "~2.7 || >=3.4"}
{
"info": {
"author": "Python Packaging Authority",
"author_email": "distutils-sig@python.org",
"bugtrack_url": "",
"classifiers": [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Archiving :: Packaging",
"Topic :: System :: Systems Administration",
"Topic :: Utilities"
],
"description": "",
"description_content_type": "text/x-rst; charset=UTF-8",
"docs_url": null,
"download_url": "",
"downloads": {
"last_day": -1,
"last_month": -1,
"last_week": -1
},
"home_page": "https://github.com/pypa/setuptools",
"keywords": "CPAN PyPI distutils eggs package management",
"license": "",
"maintainer": "",
"maintainer_email": "",
"name": "setuptools",
"package_url": "https://pypi.org/project/setuptools/",
"platform": "",
"project_url": "https://pypi.org/project/setuptools/",
"release_url": "https://pypi.org/project/setuptools/39.2.0/",
"requires_dist": [
"wincertstore (==0.2); (sys_platform=='win32') and extra == 'ssl'",
"certifi (==2016.9.26); extra == 'certs'"
],
"requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*",
"summary": "Easily download, build, install, upgrade, and uninstall Python packages",
"version": "39.2.0"
},
"last_serial": 3879671,
"releases": {
"39.2.0": [
{
"comment_text": "",
"digests": {
"md5": "8d066d2201311ed30be535b473e32fed",
"sha256": "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926"
},
"downloads": -1,
"filename": "setuptools-39.2.0-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "8d066d2201311ed30be535b473e32fed",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"size": 567556,
"upload_time": "2018-05-19T19:19:22",
"url": "https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "dd4e3fa83a21bf7bf9c51026dc8a4e59",
"sha256": "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2"
},
"downloads": -1,
"filename": "setuptools-39.2.0.zip",
"has_sig": false,
"md5_digest": "dd4e3fa83a21bf7bf9c51026dc8a4e59",
"packagetype": "sdist",
"python_version": "source",
"size": 851112,
"upload_time": "2018-05-19T19:19:24",
"url": "https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip"
}
]
},
"urls": [
{
"comment_text": "",
"digests": {
"md5": "8d066d2201311ed30be535b473e32fed",
"sha256": "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926"
},
"downloads": -1,
"filename": "setuptools-39.2.0-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "8d066d2201311ed30be535b473e32fed",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"size": 567556,
"upload_time": "2018-05-19T19:19:22",
"url": "https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "dd4e3fa83a21bf7bf9c51026dc8a4e59",
"sha256": "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2"
},
"downloads": -1,
"filename": "setuptools-39.2.0.zip",
"has_sig": false,
"md5_digest": "dd4e3fa83a21bf7bf9c51026dc8a4e59",
"packagetype": "sdist",
"python_version": "source",
"size": 851112,
"upload_time": "2018-05-19T19:19:24",
"url": "https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip"
}
]
}
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