Skip to content

Commit 07303c7

Browse files
author
Christopher Doris
committed
merges compat entries
1 parent 6a5c496 commit 07303c7

File tree

2 files changed

+201
-50
lines changed

2 files changed

+201
-50
lines changed

juliacall/deps.py

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import os
2-
# import semantic_version as semver
32
import sys
43

54
from time import time
65

76
from . import CONFIG, __version__
7+
from .jlcompat import JuliaCompat
88

99
### META
1010

@@ -43,24 +43,6 @@ def set_meta(*args):
4343
meta2[keys[-1]] = value
4444
save_meta(meta)
4545

46-
### VERSION PARSING
47-
48-
# @semver.base.BaseSpec.register_syntax
49-
# class JuliaVersionSpec(semver.SimpleSpec):
50-
# SYNTAX = 'julia'
51-
# class Parser(semver.SimpleSpec.Parser):
52-
# PREFIX_ALIASES = {'=': '==', '': '^'}
53-
# @classmethod
54-
# def parse(cls, expression):
55-
# blocks = expression.split(',')
56-
# clause = semver.base.Never()
57-
# for block in blocks:
58-
# block = block.strip()
59-
# if not cls.NAIVE_SPEC.match(block):
60-
# raise ValueError('Invalid simple block %r' % block)
61-
# clause |= cls.parse_block(block)
62-
# return clause
63-
6446
### RESOLVE
6547

6648
class PackageSpec:
@@ -149,42 +131,73 @@ def deps_files():
149131
return list(set(ans))
150132

151133
def required_packages():
134+
# read all dependencies into a dict: name -> key -> file -> value
152135
import json
153-
ans = {}
136+
all_deps = {}
154137
for fn in deps_files():
155138
with open(fn) as fp:
156139
deps = json.load(fp)
157-
for (name, kw) in deps.get("packages", {}).items():
158-
if name in ans:
159-
p = ans[name]
160-
if p.uuid != kw["uuid"]:
161-
raise Exception("found multiple UUIDs for package '{}'".format(name))
162-
if "dev" in kw:
163-
p.dev |= kw["dev"]
164-
if "compat" in kw:
165-
if p.compat is not None and p.compat != kw["compat"]:
166-
raise NotImplementedError("found multiple 'compat' entries for package '{}'".format(name))
167-
p.compat = kw["compat"]
168-
if "path" in kw:
169-
if p.path is not None and p.path != kw["path"]:
170-
raise Exception("found multiple 'path' entries for package '{}'".format(name))
171-
p.path = kw["path"]
172-
if "url" in kw:
173-
if p.url is not None and p.url != kw["url"]:
174-
raise Exception("found multiple 'url' entries for package '{}'".format(name))
175-
p.url = kw["url"]
176-
if "rev" in kw:
177-
if p.rev is not None and p.rev != kw["rev"]:
178-
raise Exception("found multiple 'rev' entries for package '{}'".format(name))
179-
p.rev = kw["rev"]
180-
if "version" in kw:
181-
if p.version is not None and p.version != kw["version"]:
182-
raise NotImplementedError("found multiple 'version' entries for package '{}'".format(name))
183-
p.version = kw["version"]
140+
for (name, kvs) in deps.get("packages", {}).items():
141+
dep = all_deps.setdefault(name, {})
142+
for (k, v) in kvs.items():
143+
dep.setdefault(k, {})[fn] = v
144+
# merges non-unique values
145+
def merge_unique(dep, kfvs, k):
146+
fvs = kfvs.pop(k, None)
147+
if fvs is not None:
148+
vs = set(fvs.values())
149+
if len(vs) == 1:
150+
dep[k], = vs
151+
elif vs:
152+
raise Exception("'{}' entries are not unique:\n{}".format(k, '\n'.join(['- {!r} at {}'.format(v,f) for (f,v) in fvs.items()])))
153+
# merges compat entries
154+
def merge_compat(dep, kfvs, k):
155+
fvs = kfvs.pop(k, None)
156+
if fvs is not None:
157+
compats = list(map(JuliaCompat, fvs.values()))
158+
compat = compats[0]
159+
for c in compats[1:]:
160+
compat &= c
161+
if compat.isempty():
162+
raise Exception("'{}' entries have empty intersection:\n{}".format(k, '\n'.join(['- {!r} at {}'.format(v,f) for (f,v) in fvs.items()])))
184163
else:
185-
p = PackageSpec(name=name, **kw)
186-
ans[p.name] = p
187-
return list(ans.values())
164+
dep[k] = compat.jlstr()
165+
# merges booleans with any
166+
def merge_any(dep, kfvs, k):
167+
fvs = kfvs.pop(k, None)
168+
if fvs is not None:
169+
dep[k] = any(fvs.values())
170+
# merge dependencies: name -> key -> value
171+
deps = []
172+
for (name, kfvs) in all_deps.items():
173+
kw = {'name': name}
174+
merge_unique(kw, kfvs, 'uuid')
175+
merge_unique(kw, kfvs, 'path')
176+
merge_unique(kw, kfvs, 'url')
177+
merge_unique(kw, kfvs, 'rev')
178+
merge_compat(kw, kfvs, 'compat')
179+
merge_any(kw, kfvs, 'dev')
180+
deps.append(PackageSpec(**kw))
181+
return deps
182+
183+
def required_julia():
184+
import json
185+
compats = {}
186+
for fn in deps_files():
187+
with open(fn) as fp:
188+
deps = json.load(fp)
189+
c = deps.get("julia")
190+
if c is not None:
191+
compats[fn] = JuliaCompat(c)
192+
compat = None
193+
for c in compats.values():
194+
if compat is None:
195+
compat = c
196+
else:
197+
compat &= c
198+
if compat is not None and compat.isempty():
199+
raise Exception("'julia' compat entries have empty intersection:\n{}".format('\n'.join(['- {!r} at {}'.format(v,f) for (f,v) in compats.items()])))
200+
return compat
188201

189202
def record_resolve(pkgs):
190203
set_meta("pydeps", {

juliacall/jlcompat.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
class JuliaCompat:
2+
def __init__(self, src):
3+
if isinstance(src, str):
4+
self.parts = self.parse_parts(src)
5+
else:
6+
self.parts = list(src)
7+
def parse_parts(self, src):
8+
return [self.parse_part(part) for part in src.split(',')]
9+
def parse_part(self, src):
10+
src = src.strip()
11+
if src.startswith('='):
12+
return self.parse_eq(src[1:])
13+
elif src.startswith('^'):
14+
return self.parse_caret(src[1:])
15+
elif src.startswith('~'):
16+
return self.parse_tilde(src[1:])
17+
else:
18+
return self.parse_caret(src)
19+
def parse_eq(self, src):
20+
return Eq(Version(src))
21+
def parse_tilde(self, src):
22+
v = Version(src)
23+
if len(v.parts) == 1:
24+
return Range(v, v.next_major())
25+
else:
26+
return Range(v, v.next_minor())
27+
def parse_caret(self, src):
28+
v = Version(src)
29+
if v.major or v.minor is None:
30+
return Range(v, v.next_major())
31+
elif v.minor or v.patch is None:
32+
return Range(v, v.next_minor())
33+
else:
34+
return Range(v, v.next_patch())
35+
def jlstr(self):
36+
return ', '.join(p.jlstr() for p in self.parts)
37+
def isempty(self):
38+
return all(p.isempty() for p in self.parts)
39+
def __and__(self, other):
40+
parts = []
41+
for p1 in self.parts:
42+
for p2 in other.parts:
43+
p3 = p1 & p2
44+
if not p3.isempty():
45+
parts.append(p3)
46+
return JuliaCompat(parts)
47+
def __repr__(self):
48+
return 'JuliaCompat({!r})'.format(self.parts)
49+
50+
class Version:
51+
def __init__(self, src):
52+
if isinstance(src, Version):
53+
src = src.parts
54+
elif isinstance(src, str):
55+
src = src.strip().split('.')
56+
if 1 <= len(src) <= 3:
57+
self.parts = tuple(map(int, src))
58+
else:
59+
raise Exception("invalid version")
60+
def __repr__(self):
61+
return 'Version({!r})'.format(self.parts)
62+
@property
63+
def major(self):
64+
return self.parts[0]
65+
@property
66+
def minor(self):
67+
return self.parts[1] if len(self.parts) > 1 else None
68+
@property
69+
def patch(self):
70+
return self.parts[2] if len(self.parts) > 2 else None
71+
def jlstr(self):
72+
return '.'.join(map(str, self.parts))
73+
def pad_zeros(self):
74+
return type(self)((self.major or 0, self.minor or 0, self.patch or 0))
75+
def next_major(self):
76+
return type(self)((self.major+1, 0, 0))
77+
def next_minor(self):
78+
return type(self)((self.major, (self.minor or 0)+1, 0))
79+
def next_patch(self):
80+
return type(self)((self.major, self.minor or 0, (self.patch or 0)+1))
81+
def __eq__(self, other):
82+
return self.parts == other.parts
83+
def __le__(self, other):
84+
return self.parts <= other.parts
85+
def __lt__(self, other):
86+
return self.parts < other.parts
87+
88+
class Eq:
89+
def __init__(self, v):
90+
self.v = v.pad_zeros()
91+
def jlstr(self):
92+
return '=' + self.v.jlstr()
93+
def isempty(self):
94+
return False
95+
def __and__(self, other):
96+
if self in other:
97+
return self
98+
else:
99+
return Range(Version('0'), Version('0'))
100+
def __rand__(self, other):
101+
return self.__and__(other)
102+
def __contains__(self, v):
103+
return self.v == v
104+
def __repr__(self):
105+
return 'Eq({!r})'.format(self.v)
106+
107+
class Range:
108+
def __init__(self, v0, v1):
109+
self.v0 = v0.pad_zeros()
110+
self.v1 = v1.pad_zeros()
111+
def jlstr(self):
112+
if self.v1 == self.v0.next_major():
113+
if self.v0.major:
114+
return self.v0.jlstr()
115+
elif self.v0.minor == 0 and self.v0.patch == 0:
116+
return '0'
117+
elif self.v1 == self.v0.next_minor():
118+
if self.v0.major == 0:
119+
if self.v0.minor:
120+
return self.v0.jlstr()
121+
elif self.v0.patch == 0:
122+
return '0.0'
123+
return '~' + self.v0.jlstr()
124+
elif self.v1 == self.v0.next_patch():
125+
if self.v0.major == 0 and self.v0.minor == 0:
126+
return self.v0.jlstr()
127+
raise ValueError("cannot represent range [{}, {}) as a compat entry".format(self.v0.jlstr(), self.v1.jlstr()))
128+
def isempty(self):
129+
return self.v1 <= self.v0
130+
def __and__(self, other):
131+
if isinstance(other, Range):
132+
return Range(max(self.v0, other.v0), min(self.v1, other.v1))
133+
else:
134+
return NotImplemented
135+
def __contains__(self, v):
136+
return self.v0 <= v and v < self.v1
137+
def __repr__(self):
138+
return 'Range({!r}, {!r})'.format(self.v0, self.v1)

0 commit comments

Comments
 (0)