Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RF: satisfies, diff and alike #273

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0325398
Dummy empty commit to facilitate long lived PR
yarikoptic Jan 17, 2019
d822155
Merge branch 'master' into nf-diff
chaselgrove Feb 7, 2019
1d33124
Merge branch 'master' into nf-diff
chaselgrove Feb 12, 2019
2fe2198
WIP: added venv to diff
chaselgrove Feb 20, 2019
f0548ce
Merge branch 'master' into rf-diff
chaselgrove Feb 21, 2019
0e555da
Merge branch 'master' into rf-diff
chaselgrove Mar 7, 2019
4f84955
Merge branch 'master' into rf-diff
chaselgrove Mar 14, 2019
ec5f058
Merge branch 'master' into rf-diff
chaselgrove Mar 20, 2019
66d8b67
replacing SpecObject._comparison_fields with _diff_cmp_fields + _diff…
chaselgrove Mar 20, 2019
3ab7a04
refactored _satisfied_by() and _identical_to() in SpecObject
chaselgrove Mar 21, 2019
9599683
added class SpecDiff
chaselgrove Mar 21, 2019
eaf140d
added _collection_attribute to CondaDistribution, GitDistribution, SV…
chaselgrove Apr 2, 2019
029dd3d
using SpecDiff for diff interface
chaselgrove Apr 2, 2019
1ac7a92
Merge branch 'master' into rf-diff
chaselgrove Apr 2, 2019
17525c7
supporting files lists in SpecDiff
chaselgrove Apr 16, 2019
138c2a6
BF: in SpecDiff, checking for collection attribute in specobject clas…
chaselgrove Apr 16, 2019
39f5f4e
added SpecDiff attributes a_only, b_only, and diffs
chaselgrove Apr 16, 2019
cf5db61
in SpecDiff:
chaselgrove Apr 17, 2019
df6d69d
added venv hierarchy to diff
chaselgrove Apr 17, 2019
c502961
Merge branch 'master' into rf-diff
chaselgrove Apr 17, 2019
f0329c9
Merge branch 'master' into rf-diff
chaselgrove Apr 18, 2019
fc4a848
allowing two empty lists to be given to SpecDiff
chaselgrove May 16, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 119 additions & 20 deletions reproman/distributions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ class SpecObject(object):
_diff_cmp_fields = tuple()
# Fields of the primary interest when showing diff
_diff_fields = tuple()
# Fields used in determination of comparison (satisfied_by and identical_to)
_comparison_fields = tuple()

@property
def _diff_cmp_id(self):
Expand All @@ -60,12 +58,19 @@ def _diff_cmp_id(self):

@property
def _cmp_id(self):
if not self._comparison_fields:
if not self._diff_cmp_fields:
# Might need to be gone or some custom exception
raise RuntimeError(
"Cannot establish identity of %r since _comaprison_fields "
"Cannot establish identity of %r since _diff_cmp_fields "
"are not defined" % self)
return tuple(getattr(self, a) for a in self._comparison_fields)
if not self._diff_fields:
# Might need to be gone or some custom exception
raise RuntimeError(
"Cannot establish identity of %r since _diff_fields "
"are not defined" % self)
vals = [ getattr(self, a) for a in self._diff_cmp_fields ]
vals.extend([ getattr(self, a) for a in self._diff_fields ])
return tuple(vals)

@property
def _diff_vals(self):
Expand Down Expand Up @@ -94,8 +99,8 @@ def diff_subidentity_string(self):

@property
def identity_string(self):
"""like diff_identity_string, but for _comparison_fields (used in
satisfied_by comparisons)
"""like diff_identity_string, but for both _diff_cmp_fields and
_diff_fields (used in satisfied_by comparisons)
"""
return " ".join(str(el) for el in self._cmp_id if el is not None)

Expand Down Expand Up @@ -137,15 +142,15 @@ def _satisfied_by(self, other):
spec object.

We require that the values of the attributes given by
_comparison_fields are the same. A specobject with a value of None
for one of these attributes is less specific than one with
a specific value; the former cannot satisfy the latter,
but the latter can satisfy the former.
_diff_cmp_fields and _diff_fields are the same. A specobject
with a value of None for one of these attributes is less specific
than one with a specific value; the former cannot satisfy the
latter, but the latter can satisfy the former.

TODO: Ensure we don't encounter the case where self is completely
unspecified (all values are None), in which case satisfied_by()
returns True by default. Perhaps this is done by making
sure that at least one of the _comparison_fields cannot be None.
sure that at least one of the _diff_cmp_fields cannot be None.

TODO: derive _collection_type directly from _collection. This isn't
possible at the moment because DebianDistribution.packages is
Expand All @@ -162,9 +167,7 @@ def _satisfied_by(self, other):
raise TypeError('don''t know how to determine if a %s is satisfied by a %s' % (self.__class__, other_collection_type))
if not isinstance(other, self.__class__):
raise TypeError('incompatible specobject types')
for attr_name in self._comparison_fields:
self_value = getattr(self, attr_name)
other_value = getattr(other, attr_name)
for (self_value, other_value) in zip(self._cmp_id, other._cmp_id):
if self_value is None:
continue
if self_value != other_value:
Expand All @@ -176,14 +179,12 @@ def _identical_to(self, other):
"""Determine if the other object is identical to the spec object.

We require that the objects are of the same type and that the
values of the attributes given by _comparison_fields are the same.
values of the attributes given by _diff_cmp_fields and _diff_fields
(dereferenced in _cmp_id) are the same.
"""
if not isinstance(other, self.__class__):
return False
for attr_name in self._comparison_fields:
if getattr(self, attr_name) != getattr(other, attr_name):
return False
return True
return all(sv==ov for sv, ov in zip(self._cmp_id, other._cmp_id))


def _register_with_representer(cls):
Expand Down Expand Up @@ -423,3 +424,101 @@ def _create_package(self, **package_fields):
provided by _get_packagefields_for_files
"""
return


class SpecDiff:

"""Difference object for SpecObjects.

Instantiate with SpecDiff(a, b). a and b must be of the same type
or TypeError is raised. Either (but not both) may be None.

Attributes:

a, b: The two objects being compared.

If _diff_cmp_fields is defined for the SpecObjects:

diff_cmp_id: The _diff_cmp_id of the two objects. If
_diff_cmp_id are different for the two objects,
they cannot be compared and ValueError is raised.

diff_vals_a, diff_vals_b: _diff_vals for a and b, respectively,
or None if a or b is None.

For collection SpecObjects (e.g. DebianDistribution, containing
DEBPackages; these have _collection_attribute defined), we also
have:

collection: a list of SpecDiff objects for the contained
SpecObjects.

a_only: SpecDiff objects from collection that only appear in the
first passed SpecObject

b_only: SpecDiff objects from collection that only appear in the
second passed SpecObject

a_and_b: SpecDiff objects in collection that appear in both
passed SpecObjects

If a and b are lists, they are treated as files specifications, and
self.collection is a list of (fname_a, fname_b) tuples.
TODO: give files and file collections their own specobjects
"""

def __init__(self, a, b):
if not isinstance(a, (SpecObject, list, type(None))) \
or not isinstance(b, (SpecObject, list, type(None))):
raise TypeError('objects must be SpecObjects or None')
if a is None and b is None:
raise TypeError('objects cannot both be None')
if a and b and type(a) != type(b):
raise TypeError('objects must be of the same type')
self.cls = type(a) if a is not None else type(b)
self.a = a
self.b = b
if self.cls == list:
a_collection = set(a)
b_collection = set(b)
self.collection = []
for fname in set(a_collection).union(b_collection):
if fname not in a_collection:
self.collection.append((None, fname))
elif fname not in b_collection:
self.collection.append((fname, None))
else:
self.collection.append((fname, fname))
else:
if self.cls._diff_cmp_fields:
if a and b and a._diff_cmp_id != b._diff_cmp_id:
raise ValueError('objects\' _diff_cmp_id differ')
self.diff_vals_a = a._diff_vals if a else None
self.diff_vals_b = b._diff_vals if b else None
self.diff_cmp_id = a._diff_cmp_id if a else b._diff_cmp_id
if hasattr(self.cls, '_collection_attribute'):
self.collection = []
a_collection = { c._diff_cmp_id: c for c in a.collection } if a else {}
b_collection = { c._diff_cmp_id: c for c in b.collection } if b else {}
all_cmp_ids = set(a_collection).union(b_collection)
for cmp_id in all_cmp_ids:
ac = a_collection[cmp_id] if cmp_id in a_collection else None
bc = b_collection[cmp_id] if cmp_id in b_collection else None
self.collection.append(SpecDiff(ac, bc))
if hasattr(self, 'collection'):
self.a_only = []
self.b_only = []
self.a_and_b = []
for pkg_diff in self.collection:
if isinstance(pkg_diff, tuple):
(a, b) = pkg_diff
else:
a = pkg_diff.a
b = pkg_diff.b
if not a:
self.b_only.append(pkg_diff)
elif not b:
self.a_only.append(pkg_diff)
else:
self.a_and_b.append(pkg_diff)
return
1 change: 1 addition & 0 deletions reproman/distributions/conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class CondaDistribution(Distribution):
environments = TypedList(CondaEnvironment)

_cmp_field = ('path',)
_collection_attribute = 'packages'

def initiate(self, environment):
"""
Expand Down
1 change: 0 additions & 1 deletion reproman/distributions/debian.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ class DEBPackage(Package):

_diff_cmp_fields = ('name', 'architecture')
_diff_fields = ('version',)
_comparison_fields = ('name', 'architecture', 'version')

_register_with_representer(DEBPackage)

Expand Down
3 changes: 2 additions & 1 deletion reproman/distributions/redhat.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ class RPMPackage(Package):
vendor = attrib()
url = attrib()
files = attrib(default=attr.Factory(list), hash=False)
_comparison_fields = ('name', 'version', 'architecture')
_diff_cmp_fields = ('name', 'architecture')
_diff_fields = ('version',)


_register_with_representer(RPMPackage)
Expand Down
4 changes: 4 additions & 0 deletions reproman/distributions/vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ class GitDistribution(VCSDistribution):
_cmd = "git"
packages = TypedList(GitRepo)

_collection_attribute = 'packages'

def initiate(self, session=None):
pass

Expand Down Expand Up @@ -296,6 +298,8 @@ class SVNDistribution(VCSDistribution):
_cmd = "svn"
packages = TypedList(SVNRepo)

_collection_attribute = 'packages'

def install_packages(self, session, use_version=True):
raise NotImplementedError
SVNRepo._distribution = SVNDistribution
Expand Down
5 changes: 5 additions & 0 deletions reproman/distributions/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ class VenvPackage(Package):
location = attrib()
editable = attrib(default=False)
files = attrib(default=attr.Factory(list))
_diff_cmp_fields = ('name', )
_diff_fields = ('version', )


@attr.s
class VenvEnvironment(SpecObject):
path = attrib()
python_version = attrib()
packages = TypedList(VenvPackage)
_diff_cmp_fields = ('path', 'python_version')
_collection_attribute = 'packages'


@attr.s
Expand All @@ -54,6 +58,7 @@ class VenvDistribution(Distribution):
path = attrib()
venv_version = attrib()
environments = TypedList(VenvEnvironment)
_collection_attribute = 'environments'

def initiate(self, _):
return
Expand Down
Loading