Skip to content

Commit

Permalink
Simple implementation of merging requires-python
Browse files Browse the repository at this point in the history
Collect them during marker tracking phase, and join them into the markers
when we format it.

The merging logic is quite stupid right now, and the resulted markers can
be very verbose (but should work).
  • Loading branch information
uranusjr committed Aug 17, 2018
1 parent d339019 commit 2d90438
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 97 deletions.
4 changes: 2 additions & 2 deletions src/passa/locking.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .caches import HashCache
from .hashes import get_hashes
from .markers import set_markers
from .metadata import set_metadata
from .providers import RequirementsLibProvider
from .reporters import StdOutReporter
from .traces import trace_graph
Expand Down Expand Up @@ -37,7 +37,7 @@ def resolve_requirements(requirements, sources, allow_pre):
for r in state.mapping.values():
r.hashes = get_hashes(hash_cache, r)

set_markers(
set_metadata(
state.mapping, traces, requirements,
provider.fetched_dependencies, provider.requires_pythons,
)
Expand Down
95 changes: 0 additions & 95 deletions src/passa/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,101 +53,6 @@ def get_without_extra(marker):
return None


def _markerset(*markers):
return frozenset(markers)


def _add_markersets(candidates, key, trace, all_markersets):
markersets = set()
for route in trace:
parent = route[-1]
try:
parent_markersets = all_markersets[parent]
except KeyError: # Parent not calculated yet. Wait for it.
return False
r = candidates[parent][key]
marker = get_without_extra(r.markers)
if marker:
markerset = _markerset(str(marker))
else:
markerset = _markerset()
markersets.update({
parent_markerset | markerset
for parent_markerset in parent_markersets
})
try:
current_markersets = all_markersets[key]
except KeyError:
all_markersets[key] = markersets
else:
all_markersets[key] = current_markersets | markersets
return True


def _calculate_markersets_mapping(requirements, candidates, traces):
all_markersets = {}

# Populate markers from Pipfile.
for r in requirements:
if r.markers:
markerset = _markerset(r.markers)
else:
markerset = _markerset()
all_markersets[identify_requirment(r)] = {markerset}

traces = copy.deepcopy(traces)
del traces[None]
while traces:
successful_keys = set()
for key, trace in traces.items():
ok = _add_markersets(candidates, key, trace, all_markersets)
if not ok:
continue
successful_keys.add(key)
if not successful_keys:
break # No progress? Deadlocked. Give up.
for key in successful_keys:
del traces[key]

return all_markersets


def set_markers(candidates, traces, requirements, dependencies, pythons):
"""Add markers to candidates based on the dependency tree.
:param candidates: A key-candidate mapping. Candidates in the mapping will
have their markers set.
:param traces: A graph trace (produced by `traces.trace_graph`) providing
information about dependency relationships between candidates.
:param requirements: A collection of requirements that was originally
provided to be resolved.
:param dependencies: A key-collection mapping containing what dependencies
each candidate in `candidates` requested.
:param pythons: A key-str mapping containing Requires-Python information
of each candidate.
Keys in mappings and entries in the trace are identifiers of a package, as
implemented by the `identify` method of the resolver's provider.
The candidates are modified in-place.
"""
markersets_mapping = _calculate_markersets_mapping(
requirements, dependencies, traces,
)
for key, candidate in candidates.items():
markersets = markersets_mapping[key]

# If there is an unconditional route, this needs to be unconditional.
if any(not s for s in markersets):
candidate.markers = None
continue

# This extra str(Marker()) call helps simplify the expression.
candidate.markers = str(Marker(" or ".join("({0})".format(
" and ".join("({0})".format(marker) for marker in markerset)
) for markerset in markersets)))


def _markers_collect_extras(markers, collection):
# Optimization: the marker element is usually appended at the end.
for el in reversed(markers):
Expand Down
151 changes: 151 additions & 0 deletions src/passa/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import copy
import itertools

from packaging.markers import Marker
from packaging.specifiers import SpecifierSet

from .markers import get_without_extra
from .utils import identify_requirment


def dedup_markers(s):
# TODO: Implement better logic.
return sorted(set(s))


class MetaSet(object):
"""Representation of a "metadata set".
This holds multiple metadata representaions. Each metadata representation
includes a marker, and a specifier set of Python versions required.
"""
def __init__(self):
self.markerset = frozenset()
self.pyspecset = SpecifierSet()

def __repr__(self):
return "MetaSet(markerset={0!r}, pyspecset={1!r})".format(
",".join(sorted(self.markerset)), self.pyspecset,
)

def __str__(self):
return " and ".join(dedup_markers(itertools.chain(
(
"({0})".format(m) if " or " in m else m
for m in (str(marker) for marker in self.markerset)
),
( # Use double quotes (packaging's format) so we can dedup.
'python_version {0[0]} "{0[1]}"'.format(spec._spec)
for spec in self.pyspecset._specs
),
)))

def __bool__(self):
return bool(self.markerset or self.pyspecset)

def __nonzero__(self): # Python 2.
return self.__bool__()

def __or__(self, pair):
marker, specset = pair
markerset = set(self.markerset)
if marker:
markerset.add(str(marker))
metaset = MetaSet()
metaset.markerset = frozenset(markerset)
metaset.pyspecset &= self.pyspecset & specset
return metaset


def _add_metasets(candidates, pythons, key, trace, all_metasets):
metaset_iters = []
for route in trace:
parent = route[-1]
try:
parent_metasets = all_metasets[parent]
except KeyError: # Parent not calculated yet. Wait for it.
return False
r = candidates[parent][key]
python = pythons[parent]
metaset = (get_without_extra(r.markers), SpecifierSet(python))
metaset_iters.append(
parent_metaset | metaset
for parent_metaset in parent_metasets
)
metasets = list(itertools.chain.from_iterable(metaset_iters))
try:
current = all_metasets[key]
except KeyError:
all_metasets[key] = metasets
else:
all_metasets[key] = current + metasets
return True


def _calculate_metasets_mapping(requirements, candidates, pythons, traces):
all_metasets = {}

# Populate metadata from Pipfile.
for r in requirements:
specifiers = r.specifiers or SpecifierSet()
metaset = MetaSet() | (get_without_extra(r.markers), specifiers)
all_metasets[identify_requirment(r)] = [metaset]

traces = copy.deepcopy(traces)
del traces[None]
while traces:
successful_keys = set()
for key, trace in traces.items():
successful = _add_metasets(
candidates, pythons, key, trace, all_metasets,
)
if not successful:
continue
successful_keys.add(key)
if not successful_keys:
break # No progress? Deadlocked. Give up.
for key in successful_keys:
del traces[key]

return all_metasets


def _format_metasets(metasets):
# If there is an unconditional route, this needs to be unconditional.
if not metasets or not all(metasets):
return None

# This extra str(Marker()) call helps simplify the expression.
return str(Marker(" or ".join(
"({0})".format(s) if " and " in s else s
for s in dedup_markers(str(metaset) for metaset in metasets)
)))


def set_metadata(candidates, traces, requirements, dependencies, pythons):
"""Add "metadata" to candidates based on the dependency tree.
Metadata for a candidate includes markers and a specifier for Python
version requirements.
:param candidates: A key-candidate mapping. Candidates in the mapping will
have their markers set.
:param traces: A graph trace (produced by `traces.trace_graph`) providing
information about dependency relationships between candidates.
:param requirements: A collection of requirements that was originally
provided to be resolved.
:param dependencies: A key-collection mapping containing what dependencies
each candidate in `candidates` requested.
:param pythons: A key-str mapping containing Requires-Python information
of each candidate.
Keys in mappings and entries in the trace are identifiers of a package, as
implemented by the `identify` method of the resolver's provider.
The candidates are modified in-place.
"""
metasets_mapping = _calculate_metasets_mapping(
requirements, dependencies, pythons, traces,
)
for key, candidate in candidates.items():
candidate.markers = _format_metasets(metasets_mapping[key])

0 comments on commit 2d90438

Please sign in to comment.