Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
python-poetry
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
open
python-poetry
Commits
f75b1cb7
Unverified
Commit
f75b1cb7
authored
Feb 25, 2018
by
Sébastien Eustace
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix dependency resolution
parent
7e4668ea
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
192 additions
and
41 deletions
+192
-41
poetry/mixology/contracts/specification_provider.py
+1
-1
poetry/mixology/contracts/ui.py
+1
-1
poetry/mixology/exceptions.py
+1
-1
poetry/mixology/graph/add_edge_no_circular.py
+4
-1
poetry/mixology/graph/add_vertex.py
+4
-2
poetry/mixology/graph/delete_edge.py
+4
-1
poetry/mixology/graph/vertex.py
+1
-1
poetry/mixology/possibility_set.py
+3
-0
poetry/mixology/resolution.py
+67
-26
poetry/mixology/state.py
+7
-4
poetry/mixology/unwind_details.py
+13
-3
tests/mixology/fixtures/case/empty.json
+9
-0
tests/mixology/fixtures/case/unresolvable_child.json
+12
-0
tests/mixology/fixtures/index/unresolvable_child.json
+38
-0
tests/mixology/test_resolver.py
+27
-0
No files found.
poetry/mixology/contracts/specification_provider.py
View file @
f75b1cb7
...
...
@@ -36,7 +36,7 @@ class SpecificationProvider:
def
dependencies_for
(
self
,
specification
:
Any
)
->
List
[
Any
]:
"""
Returns the dependencies of
`specification`
.
Returns the dependencies of
specification
.
"""
return
[]
...
...
poetry/mixology/contracts/ui.py
View file @
f75b1cb7
...
...
@@ -26,7 +26,7 @@ class UI:
def
after_resolution
(
self
)
->
None
:
self
.
output
.
write
(
''
)
def
debug
(
self
,
depth
,
message
)
->
None
:
def
debug
(
self
,
message
,
depth
)
->
None
:
if
self
.
is_debugging
():
debug_info
=
str
(
message
)
debug_info
=
'
\n
'
.
join
([
...
...
poetry/mixology/exceptions.py
View file @
f75b1cb7
...
...
@@ -44,7 +44,7 @@ class VersionConflict(ResolverError):
for
conflicting
in
flat_map
(
list
(
conflicts
.
values
()),
lambda
x
:
x
.
requirements
):
for
source
,
conflict_requirements
in
conflicting
:
for
source
,
conflict_requirements
in
conflicting
.
items
()
:
for
c
in
conflict_requirements
:
pairs
.
append
((
c
,
source
))
...
...
poetry/mixology/graph/add_edge_no_circular.py
View file @
f75b1cb7
...
...
@@ -50,7 +50,10 @@ class AddEdgeNoCircular(Action):
"""
:type elements: list
"""
index
=
elements
.
index
(
element
)
try
:
index
=
elements
.
index
(
element
)
except
ValueError
:
return
if
index
!=
-
1
:
elements
.
pop
(
index
)
poetry/mixology/graph/add_vertex.py
View file @
f75b1cb7
from
.action
import
Action
from
.vertex
import
Vertex
_NULL
=
object
()
class
AddVertex
(
Action
):
...
...
@@ -20,7 +22,7 @@ class AddVertex(Action):
self
.
_name
=
name
self
.
_payload
=
payload
self
.
_root
=
root
self
.
_existing_payload
=
None
self
.
_existing_payload
=
_NULL
self
.
_existing_root
=
None
@property
...
...
@@ -56,7 +58,7 @@ class AddVertex(Action):
return
vertex
def
down
(
self
,
graph
):
if
self
.
_existing_payload
is
not
None
:
if
self
.
_existing_payload
is
not
_NULL
:
vertex
=
graph
.
vertices
[
self
.
_name
]
vertex
.
payload
=
self
.
_existing_payload
vertex
.
root
=
self
.
_existing_root
...
...
poetry/mixology/graph/delete_edge.py
View file @
f75b1cb7
...
...
@@ -50,7 +50,10 @@ class DeleteEdge(Action):
"""
:type elements: list
"""
index
=
elements
.
index
(
element
)
try
:
index
=
elements
.
index
(
element
)
except
ValueError
:
return
if
index
!=
-
1
:
elements
.
pop
(
index
)
poetry/mixology/graph/vertex.py
View file @
f75b1cb7
...
...
@@ -62,7 +62,7 @@ class Vertex:
continue
vertices
.
add
(
vertex
)
vertex
.
_recursive_
prede
cessors
(
vertices
)
vertex
.
_recursive_
suc
cessors
(
vertices
)
return
vertices
...
...
poetry/mixology/possibility_set.py
View file @
f75b1cb7
...
...
@@ -11,3 +11,6 @@ class PossibilitySet:
def
__str__
(
self
):
return
'[{}]'
.
format
(
', '
.
join
([
repr
(
p
)
for
p
in
self
.
possibilities
]))
def
__repr__
(
self
):
return
f
'<PossibilitySet {str(self)}>'
poetry/mixology/resolution.py
View file @
f75b1cb7
import
logging
from
copy
import
copy
from
time
import
time
from
datetime
import
date
time
from
typing
import
Any
from
typing
import
List
...
...
@@ -36,8 +36,10 @@ class Resolution:
self
.
_requested
=
requested
self
.
_original_requested
=
copy
(
requested
)
self
.
_base
=
base
self
.
_states
=
[
ResolutionState
.
empty
()
]
self
.
_states
=
[]
self
.
_iteration_counter
=
0
self
.
_progress_rate
=
0.33
self
.
_iteration_rate
=
None
self
.
_parents_of
=
{}
self
.
_started_at
=
None
...
...
@@ -73,9 +75,14 @@ class Resolution:
if
not
self
.
state
.
requirement
and
not
self
.
state
.
requirements
:
break
# TODO: indicate progress
self
.
_indicate_progress
()
if
hasattr
(
self
.
state
,
'pop_possibility_state'
):
s
=
self
.
state
.
pop_possiblity_state
()
self
.
_debug
(
f
'Creating possibility state for '
f
'{str(self.state.requirement)} '
f
'({len(self.state.possibilities)} remaining)'
)
s
=
self
.
state
.
pop_possibility_state
()
if
s
:
self
.
_states
.
append
(
s
)
self
.
activated
.
tag
(
s
)
...
...
@@ -90,11 +97,14 @@ class Resolution:
"""
Set up the resolution process.
"""
self
.
_started_at
=
time
()
self
.
_started_at
=
datetime
.
now
()
self
.
_handle_missing_or_push_dependency_state
(
self
.
_initial_state
())
logger
.
debug
(
'Starting resolution'
)
self
.
_debug
(
f
'Starting resolution ({self._started_at})
\n
'
f
'Requested dependencies: {self._original_requested}'
)
self
.
_ui
.
before_resolution
()
...
...
@@ -124,12 +134,13 @@ class Resolution:
"""
Ends the resolution process
"""
elapsed
=
(
datetime
.
now
()
-
self
.
_started_at
)
.
total_seconds
()
self
.
_ui
.
after_resolution
()
logger
.
debug
(
'Finished resolution ({} steps) in {:.3f} seconds'
.
format
(
self
.
_iteration_counter
,
time
()
-
self
.
_started_at
)
self
.
_debug
(
f
'Finished resolution ({self._iteration_counter} steps) '
f
'in {elapsed:.3f} seconds'
)
def
_process_topmost_state
(
self
)
->
None
:
...
...
@@ -151,7 +162,8 @@ class Resolution:
"""
The current possibility that the resolution is trying.
"""
return
self
.
state
.
possibilities
[
-
1
]
if
self
.
state
.
possibilities
:
return
self
.
state
.
possibilities
[
-
1
]
@property
def
state
(
self
)
->
DependencyState
:
...
...
@@ -213,17 +225,21 @@ class Resolution:
unwind_options
=
self
.
state
.
unused_unwind_options
self
.
_debug
(
self
.
state
.
depth
,
'Unwinding for conflict: '
'{} to {}'
.
format
(
s
elf
.
state
.
requirement
,
s
tr
(
self
.
state
.
requirement
)
,
details_for_unwind
.
state_index
//
2
)
),
self
.
state
.
depth
)
conflicts
=
self
.
state
.
conflicts
sliced_states
=
self
.
_states
[
details_for_unwind
.
state_index
+
1
:]
self
.
_states
=
self
.
_states
[:
details_for_unwind
.
state_index
]
if
details_for_unwind
.
state_index
==
-
1
:
self
.
_states
=
[]
else
:
self
.
_states
=
self
.
_states
[:
details_for_unwind
.
state_index
]
self
.
_raise_error_unless_state
(
conflicts
)
if
sliced_states
:
self
.
activated
.
rewind_to
(
...
...
@@ -251,7 +267,7 @@ class Resolution:
return
errors
=
[
c
.
underlying_error
for
c
in
conflicts
for
c
in
conflicts
.
values
()
if
c
.
underlying_error
is
not
None
]
if
errors
:
error
=
errors
[
0
]
...
...
@@ -484,12 +500,11 @@ class Resolution:
return
satisfied
def
_filter_possibilities_for_parent_unwind
(
self
,
unwind_details
):
def
_filter_possibilities_for_parent_unwind
(
self
,
unwind_details
:
UnwindDetails
):
"""
Filter a state's possibilities to remove any that would (eventually)
the requirements in the conflict we've just rewound from.
:type unwind_details: UnwindDetails
"""
unwinds_to_state
=
[
uw
...
...
@@ -569,7 +584,10 @@ class Resolution:
# If all the requirements together don't filter out all possibilities,
# then the only two requirements we need to consider are the initial one
# (where the dependency's version was first chosen) and the last
if
self
.
_binding_requirement_in_set
(
None
,
possible_binding_requirements
,
possibilities
):
if
self
.
_binding_requirement_in_set
(
None
,
possible_binding_requirements
,
possibilities
):
return
list
(
filter
(
None
,
[
conflict
.
requirement
,
self
.
_requirement_for_existing_name
(
...
...
@@ -699,19 +717,29 @@ class Resolution:
return
tree
def
_debug
(
self
,
depth
,
message
):
self
.
_ui
.
debug
(
depth
,
message
)
def
_indicate_progress
(
self
):
self
.
_iteration_counter
+=
1
progress_rate
=
self
.
_ui
.
progress_rate
or
self
.
_progress_rate
if
self
.
_iteration_rate
is
None
:
if
(
datetime
.
now
()
-
self
.
_started_at
)
.
total_seconds
()
>=
progress_rate
:
self
.
_iteration_rate
=
self
.
_iteration_counter
if
self
.
_iteration_rate
and
(
self
.
_iteration_counter
%
self
.
_iteration_rate
)
==
0
:
self
.
_ui
.
indicate_progress
()
def
_debug
(
self
,
message
,
depth
=
0
):
self
.
_ui
.
debug
(
message
,
depth
)
def
_attempt_to_activate
(
self
):
self
.
_debug
(
f
'Attempting to activate {str(self.possibility)}'
,
self
.
state
.
depth
,
'Attempting to activate {}'
.
format
(
self
.
possibility
)
)
existing_vertex
=
self
.
activated
.
vertex_named
(
self
.
state
.
name
)
if
existing_vertex
.
payload
:
self
.
_debug
(
self
.
state
.
depth
,
'Found existing spec ({})'
.
format
(
existing_vertex
.
payload
)
'Found existing spec ({})'
.
format
(
existing_vertex
.
payload
)
,
self
.
state
.
depth
)
self
.
_attempt_to_filter_existing_spec
(
existing_vertex
)
else
:
...
...
@@ -747,6 +775,10 @@ class Resolution:
self
.
_push_state_for_requirements
(
new_requirements
,
False
)
else
:
self
.
_create_conflict
()
self
.
_debug
(
f
'Unsatisfied by existing spec ({str(vertex.payload)})'
,
self
.
state
.
depth
)
self
.
_unwind_for_conflict
()
def
_filtered_possibility_set
(
self
,
vertex
):
...
...
@@ -770,13 +802,22 @@ class Resolution:
if
self
.
state
.
name
in
self
.
state
.
conflicts
:
del
self
.
state
.
conflicts
[
self
.
name
]
self
.
activated
.
set_payload
(
self
.
name
,
self
.
possibility
)
self
.
_debug
(
f
'Activated {self.state.name} at {str(self.possibility)}'
,
self
.
state
.
depth
)
self
.
activated
.
set_payload
(
self
.
state
.
name
,
self
.
possibility
)
self
.
_require_nested_dependencies_for
(
self
.
possibility
)
def
_require_nested_dependencies_for
(
self
,
possibility_set
):
nested_dependencies
=
self
.
_provider
.
dependencies_for
(
possibility_set
.
latest_version
)
self
.
_debug
(
f
'Requiring nested dependencies '
f
'({", ".join([str(d) for d in nested_dependencies])})'
,
self
.
state
.
depth
)
for
d
in
nested_dependencies
:
self
.
activated
.
add_child_vertex
(
...
...
poetry/mixology/state.py
View file @
f75b1cb7
...
...
@@ -41,6 +41,9 @@ class ResolutionState:
def
empty
(
cls
):
return
cls
(
None
,
[],
DependencyGraph
(),
None
,
None
,
0
,
{},
[])
def
__repr__
(
self
):
return
f
'<{self.__class__.__name__} {self._name}>'
class
PossibilityState
(
ResolutionState
):
...
...
@@ -49,16 +52,16 @@ class PossibilityState(ResolutionState):
class
DependencyState
(
ResolutionState
):
def
pop_possiblity_state
(
self
):
def
pop_possib
i
lity_state
(
self
):
state
=
PossibilityState
(
self
.
_name
,
copy
(
self
.
_requirements
),
self
.
_activated
,
self
.
_requirement
,
[
self
.
_possibilities
.
pop
()
],
[
self
.
possibilities
.
pop
()
if
self
.
possibilities
else
None
],
self
.
_depth
+
1
,
copy
(
self
.
_
conflicts
),
copy
(
self
.
_
unused_unwind_options
)
copy
(
self
.
conflicts
),
copy
(
self
.
unused_unwind_options
)
)
state
.
activated
.
tag
(
state
)
...
...
poetry/mixology/unwind_details.py
View file @
f75b1cb7
from
collections
import
namedtuple
class
UnwindDetails
:
def
__init__
(
self
,
...
...
@@ -37,10 +40,14 @@ class UnwindDetails:
if
self
.
_sub_dependencies_to_avoid
is
None
:
self
.
_sub_dependencies_to_avoid
=
[]
for
tree
in
self
.
requirement_trees
:
index
=
tree
.
index
(
self
.
state_requirement
)
if
index
and
tree
[
index
+
1
]
is
not
None
:
try
:
index
=
tree
.
index
(
self
.
state_requirement
)
except
ValueError
:
continue
if
tree
[
index
+
1
]
is
not
None
:
self
.
_sub_dependencies_to_avoid
.
append
(
tree
[
index
+
1
])
return
self
.
_sub_dependencies_to_avoid
@property
...
...
@@ -89,3 +96,6 @@ class UnwindDetails:
return
NotImplemented
return
self
.
state_index
>=
other
.
state_index
def
__hash__
(
self
):
return
hash
((
id
(
self
),
self
.
state_index
,
self
.
state_requirement
))
tests/mixology/fixtures/case/empty.json
0 → 100644
View file @
f75b1cb7
{
"name"
:
"resolves an empty list of dependencies"
,
"requested"
:
{
},
"base"
:
[],
"resolved"
:
[
],
"conflicts"
:
[]
}
tests/mixology/fixtures/case/unresolvable_child.json
0 → 100644
View file @
f75b1cb7
{
"name"
:
"yields conflicts if a child dependency is not resolved"
,
"index"
:
"unresolvable_child"
,
"requested"
:
{
"chef_app_error"
:
"*"
},
"base"
:
[],
"resolved"
:
[],
"conflicts"
:
[
"json"
]
}
tests/mixology/fixtures/index/unresolvable_child.json
0 → 100644
View file @
f75b1cb7
{
"json"
:
[
{
"name"
:
"json"
,
"version"
:
"1.8.0"
,
"dependencies"
:
{
}
}
],
"chef"
:
[
{
"name"
:
"chef"
,
"version"
:
"10.26"
,
"dependencies"
:
{
"json"
:
"<= 1.7.7, >= 1.4.4"
}
}
],
"berkshelf"
:
[
{
"name"
:
"berkshelf"
,
"version"
:
"2.0.7"
,
"dependencies"
:
{
"json"
:
">= 1.7.7"
}
}
],
"chef_app_error"
:
[
{
"name"
:
"chef_app_error"
,
"version"
:
"1.0.0"
,
"dependencies"
:
{
"berkshelf"
:
"~2.0"
,
"chef"
:
"~10.26"
}
}
]
}
tests/mixology/test_resolver.py
View file @
f75b1cb7
...
...
@@ -4,7 +4,9 @@ import pytest
from
poetry.mixology
import
DependencyGraph
from
poetry.mixology
import
Resolver
from
poetry.mixology.exceptions
import
CircularDependencyError
from
poetry.mixology.exceptions
import
ResolverError
from
poetry.mixology.exceptions
import
VersionConflict
from
poetry.packages
import
Dependency
from
.index
import
Index
...
...
@@ -120,6 +122,7 @@ def assert_graph(dg, result):
@pytest.mark.parametrize
(
'fixture'
,
[
'empty'
,
'simple'
,
'simple_with_base'
,
'simple_with_dependencies'
,
...
...
@@ -132,3 +135,27 @@ def test_resolver(fixture):
dg
=
resolver
.
resolve
(
c
.
requested
,
base
=
c
.
base
)
assert_graph
(
dg
,
c
.
result
)
@pytest.mark.parametrize
(
'fixture'
,
[
'circular'
,
'unresolvable_child'
]
)
def
test_resolver_fail
(
fixture
):
c
=
case
(
fixture
)
resolver
=
Resolver
(
c
.
index
,
UI
())
with
pytest
.
raises
(
ResolverError
)
as
e
:
resolver
.
resolve
(
c
.
requested
,
base
=
c
.
base
)
names
=
[]
e
=
e
.
value
if
isinstance
(
e
,
CircularDependencyError
):
names
=
[
d
.
name
for
d
in
e
.
dependencies
]
elif
isinstance
(
e
,
VersionConflict
):
names
=
[
n
for
n
in
e
.
conflicts
.
keys
()]
assert
sorted
(
names
)
==
sorted
(
c
.
conflicts
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment