Skip to content

Commit 6288f2e

Browse files
author
Christopher Doris
committed
implement juliacall dependencies
1 parent 939f4ad commit 6288f2e

File tree

2 files changed

+182
-39
lines changed

2 files changed

+182
-39
lines changed

juliacall/deps.py

Lines changed: 137 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
from . import CONFIG
1+
import os
2+
import semantic_version as semver
3+
import sys
4+
5+
from collections import namedtuple
6+
from time import time
7+
8+
from . import CONFIG, __version__
9+
10+
### META
211

312
def load_meta():
413
import json, os.path
@@ -35,33 +44,134 @@ def set_meta(*args):
3544
meta2[keys[-1]] = value
3645
save_meta(meta)
3746

38-
def get_dep(package, name):
39-
return get_meta('pydeps', package, name)
47+
### VERSION PARSING
4048

41-
def set_dep(package, name, value):
42-
set_meta('pydeps', package, name, value)
49+
@semver.base.BaseSpec.register_syntax
50+
class JuliaVersionSpec(semver.SimpleSpec):
51+
SYNTAX = 'julia'
52+
class Parser(semver.SimpleSpec.Parser):
53+
PREFIX_ALIASES = {'=': '==', '': '^'}
54+
@classmethod
55+
def parse(cls, expression):
56+
blocks = expression.split(',')
57+
clause = semver.base.Never()
58+
for block in blocks:
59+
block = block.strip()
60+
if not cls.NAIVE_SPEC.match(block):
61+
raise ValueError('Invalid simple block %r' % block)
62+
clause |= cls.parse_block(block)
63+
return clause
4364

44-
def require(package, name, value, func=None):
45-
if func is None:
46-
return Require(package, name, value)
47-
elif get_dep(package, name) != value:
48-
func()
49-
set_dep(package, name, value)
65+
### RESOLVE
5066

51-
class Require:
52-
def __init__(self, package, name, value):
53-
self.package = package
67+
class PackageSpec:
68+
def __init__(self, name, uuid, dev=False, compat=None, path=None, url=None, rev=None, version=None):
5469
self.name = name
55-
self.value = value
56-
def __enter__(self):
57-
self.required = get_dep(self.package, self.name) != self.value
58-
return self.required
59-
def __exit__(self, t, v, b):
60-
if t is None and self.required:
61-
set_dep(self.package, self.name, self.value)
62-
63-
def require_julia(package, name, version):
64-
with require(package, name, version) as required:
65-
if required:
66-
from juliacall import Pkg
67-
Pkg.add(name=name, version=version)
70+
self.uuid = uuid
71+
self.dev = dev
72+
self.compat = compat
73+
self.path = path
74+
self.url = url
75+
self.rev = rev
76+
self.version = version
77+
78+
def jlstr(self):
79+
args = ['name="{}"'.format(self.name), 'uuid="{}"'.format(self.uuid)]
80+
if self.path is not None:
81+
args.append('path=raw"{}"'.format(self.path))
82+
if self.url is not None:
83+
args.append('url=raw"{}"'.format(self.url))
84+
if self.rev is not None:
85+
args.append('rev=raw"{}"'.format(self.rev))
86+
if self.version is not None:
87+
args.append('version=raw"{}"'.format(self.version))
88+
return "Pkg.PackageSpec({})".format(', '.join(args))
89+
90+
def dict(self):
91+
ans = {
92+
"name": self.name,
93+
"uuid": self.uuid,
94+
"dev": self.dev,
95+
"compat": self.compat,
96+
"path": self.path,
97+
"url": self.url,
98+
"rev": self.rev,
99+
"version": self.version,
100+
}
101+
return {k:v for (k,v) in ans.items() if v is not None}
102+
103+
def can_skip_resolve():
104+
# resolve if we haven't resolved before
105+
deps = get_meta("pydeps")
106+
if deps is None:
107+
return False
108+
# resolve whenever the version changes
109+
version = deps.get("version")
110+
if version is None or version != __version__:
111+
return False
112+
# resolve whenever swapping between dev/not dev
113+
isdev = deps.get("dev")
114+
if isdev is None or isdev != CONFIG["dev"]:
115+
return False
116+
# resolve whenever anything in sys.path changes
117+
timestamp = deps.get("timestamp")
118+
if timestamp is None:
119+
return False
120+
sys_path = deps.get("sys_path")
121+
if sys_path is None or sys_path != sys.path:
122+
return False
123+
for path in sys.path:
124+
if not path:
125+
path = os.getcwd()
126+
if not os.path.exists(path):
127+
continue
128+
if os.path.getmtime(path) > timestamp:
129+
return False
130+
if os.path.isdir(path):
131+
fn = os.path.join(path, "juliacalldeps.json")
132+
if os.path.exists(fn) and os.path.getmtime(fn) > timestamp:
133+
return False
134+
return True
135+
136+
def deps_files():
137+
ans = []
138+
for path in sys.path:
139+
if not path:
140+
path = os.getcwd()
141+
if not os.path.isdir(path):
142+
continue
143+
fn = os.path.join(path, "juliacalldeps.json")
144+
if os.path.isfile(fn):
145+
ans.append(fn)
146+
for subdir in os.listdir(path):
147+
fn = os.path.join(path, subdir, "juliacalldeps.json")
148+
if os.path.isfile(fn):
149+
ans.append(fn)
150+
return list(set(ans))
151+
152+
def required_packages():
153+
import json
154+
ans = {}
155+
for fn in deps_files():
156+
with open(fn) as fp:
157+
deps = json.load(fp)
158+
for (name, kw) in deps.get("packages", {}).items():
159+
if name in ans:
160+
p = ans[name]
161+
if p.uuid != kw["uuid"]:
162+
raise Exception("found multiple UUIDs for package '{}'".format(name))
163+
# todo: dev, compat, path, url, rev, version
164+
raise NotImplementedError("need to merge repeated dependency '{}'")
165+
else:
166+
p = PackageSpec(name=name, **kw)
167+
ans[p.name] = p
168+
return list(ans.values())
169+
170+
def record_resolve(pkgs):
171+
set_meta("pydeps", {
172+
"version": __version__,
173+
"dev": CONFIG["dev"],
174+
"timestamp": time(),
175+
"sys_path": sys.path,
176+
"pkgs": [pkg.dict() for pkg in pkgs],
177+
})

juliacall/init.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import os, os.path, sys, ctypes as c, types, shutil, subprocess, jill.install as jli
2-
from . import CONFIG, __version__
3-
from .deps import get_dep, set_dep
1+
import os, os.path, ctypes as c, shutil, subprocess, jill.install as jli
2+
from . import CONFIG, __version__, deps
43

54
# Determine if this is a development version of juliacall
65
# i.e. it is installed from the github repo, which contains Project.toml
@@ -36,6 +35,7 @@
3635
jlenv = os.path.join(jldepot, "environments", "PythonCall")
3736
else:
3837
jlenv = os.path.join(prefix, "julia_env")
38+
CONFIG['jlenv'] = os.path.join(jlenv)
3939
CONFIG['meta'] = os.path.join(jlenv, "PythonCallMeta.json")
4040

4141
# Find the Julia library
@@ -103,12 +103,47 @@
103103
lib.jl_init__threading()
104104
lib.jl_eval_string.argtypes = [c.c_char_p]
105105
lib.jl_eval_string.restype = c.c_void_p
106-
if isdev:
107-
install = 'Pkg.develop(path="{}")'.format(reporoot.replace('\\', '\\\\'))
108-
elif get_dep('juliacall', 'PythonCall') != __version__:
109-
install = 'Pkg.add(name="PythonCall", version="{}")'.format(__version__)
110-
else:
106+
if deps.can_skip_resolve():
107+
pkgs = None
111108
install = ''
109+
else:
110+
# get required packages
111+
pkgs = deps.required_packages()
112+
# add PythonCall
113+
if isdev:
114+
pkgs.append(deps.PackageSpec(name="PythonCall", uuid="6099a3de-0909-46bc-b1f4-468b9a2dfc0d", path=reporoot, dev=True))
115+
else:
116+
pkgs.append(deps.PackageSpec(name="PythonCall", uuid="6099a3de-0909-46bc-b1f4-468b9a2dfc0d", compat="= "+__version__))
117+
# check if pkgs has changed at all
118+
prev_pkgs = deps.get_meta("pydeps", "pkgs")
119+
if prev_pkgs is not None and sorted(prev_pkgs, key=lambda p: p['name']) == sorted([p.dict() for p in pkgs], key=lambda p: p['name']):
120+
install = ''
121+
else:
122+
# Write a Project.toml specifying UUIDs and compatibility of required packages
123+
if os.path.exists(jlenv):
124+
shutil.rmtree(jlenv)
125+
os.makedirs(jlenv)
126+
with open(os.path.join(jlenv, "Project.toml"), "wt") as fp:
127+
print('[deps]', file=fp)
128+
for pkg in pkgs:
129+
print('{} = "{}"'.format(pkg.name, pkg.uuid), file=fp)
130+
print(file=fp)
131+
print('[compat]', file=fp)
132+
for pkg in pkgs:
133+
if pkg.compat:
134+
print('{} = "{}"'.format(pkg.name, pkg.compat), file=fp)
135+
print(file=fp)
136+
# Create install command
137+
dev_pkgs = [pkg.jlstr() for pkg in pkgs if pkg.dev]
138+
add_pkgs = [pkg.jlstr() for pkg in pkgs if not pkg.dev]
139+
if dev_pkgs and add_pkgs:
140+
install = 'Pkg.develop([{}]); Pkg.add([{}])'.format(', '.join(dev_pkgs), ', '.join(add_pkgs))
141+
elif dev_pkgs:
142+
install = 'Pkg.develop([{}])'.format(', '.join(dev_pkgs))
143+
elif add_pkgs:
144+
install = 'Pkg.add([{}])'.format(', '.join(add_pkgs))
145+
else:
146+
install = ''
112147
script = '''
113148
try
114149
import Pkg
@@ -124,9 +159,7 @@
124159
res = lib.jl_eval_string(script.encode('utf8'))
125160
if res is None:
126161
raise Exception('PythonCall.jl did not start properly')
127-
if isdev:
128-
set_dep('juliacall', 'PythonCall', 'dev')
129-
elif install:
130-
set_dep('juliacall', 'PythonCall', __version__)
162+
if pkgs is not None:
163+
deps.record_resolve(pkgs)
131164
finally:
132165
os.chdir(d)

0 commit comments

Comments
 (0)