Skip to content

Commit 100dde1

Browse files
authored
Merge pull request #3101 from Suor/prepare-summon
summon: fixes and dvcx prereq
2 parents 537a0af + be90c38 commit 100dde1

File tree

1 file changed

+73
-36
lines changed

1 file changed

+73
-36
lines changed

dvc/api.py

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
1+
from builtins import open as builtin_open
12
import importlib
23
import os
34
import sys
4-
import copy
55
from urllib.parse import urlparse
66
from contextlib import contextmanager, _GeneratorContextManager as GCM
7+
import threading
78

9+
from funcy import wrap_with
810
import ruamel.yaml
911
from voluptuous import Schema, Required, Invalid
1012

1113
from dvc.repo import Repo
12-
from dvc.exceptions import DvcException, FileMissingError
14+
from dvc.exceptions import DvcException
1315
from dvc.external_repo import external_repo
1416

1517

16-
SUMMON_SCHEMA = Schema(
18+
SUMMON_FILE_SCHEMA = Schema(
1719
{
1820
Required("objects"): [
1921
{
2022
Required("name"): str,
2123
"meta": dict,
2224
Required("summon"): {
23-
Required("type"): "python",
24-
Required("call"): str,
25-
"args": dict,
25+
Required("type"): str,
2626
"deps": [str],
27+
str: object,
2728
},
2829
}
2930
]
3031
}
3132
)
33+
SUMMON_PYTHON_SCHEMA = Schema(
34+
{
35+
Required("type"): "python",
36+
Required("call"): str,
37+
"args": dict,
38+
"deps": [str],
39+
}
40+
)
3241

3342

3443
class SummonError(DvcException):
@@ -97,32 +106,74 @@ def _make_repo(repo_url, rev=None):
97106

98107
def summon(name, repo=None, rev=None, summon_file="dvcsummon.yaml", args=None):
99108
"""Instantiate an object described in the summon file."""
109+
with prepare_summon(
110+
name, repo=repo, rev=rev, summon_file=summon_file
111+
) as desc:
112+
try:
113+
summon_dict = SUMMON_PYTHON_SCHEMA(desc.obj["summon"])
114+
except Invalid as exc:
115+
raise SummonError(str(exc)) from exc
116+
117+
_args = {**summon_dict.get("args", {}), **(args or {})}
118+
return _invoke_method(summon_dict["call"], _args, desc.repo.root_dir)
119+
120+
121+
@contextmanager
122+
def prepare_summon(name, repo=None, rev=None, summon_file="dvcsummon.yaml"):
123+
"""Does a couple of things every summon needs as a prerequisite:
124+
clones the repo, parses the summon file and pulls the deps.
125+
126+
Calling code is expected to complete the summon logic following
127+
instructions stated in "summon" dict of the object spec.
128+
129+
Returns a SummonDesc instance, which contains references to a Repo object,
130+
named object specification and resolved paths to deps.
131+
"""
100132
with _make_repo(repo, rev=rev) as _repo:
101133
try:
102134
path = os.path.join(_repo.root_dir, summon_file)
103-
obj = _get_object_from_summon_file(name, path)
104-
info = obj["summon"]
135+
obj = _get_object_spec(name, path)
136+
yield SummonDesc(_repo, obj)
105137
except SummonError as exc:
106138
raise SummonError(
107139
str(exc) + " at '{}' in '{}'".format(summon_file, repo)
108-
) from exc
140+
) from exc.__cause__
141+
142+
143+
class SummonDesc:
144+
def __init__(self, repo, obj):
145+
self.repo = repo
146+
self.obj = obj
147+
self._pull_deps()
148+
149+
@property
150+
def deps(self):
151+
return [os.path.join(self.repo.root_dir, d) for d in self._deps]
152+
153+
@property
154+
def _deps(self):
155+
return self.obj["summon"].get("deps", [])
109156

110-
_pull_dependencies(_repo, info.get("deps", []))
157+
def _pull_deps(self):
158+
if not self._deps:
159+
return
111160

112-
_args = copy.deepcopy(info.get("args", {}))
113-
_args.update(args or {})
161+
outs = [self.repo.find_out_by_relpath(d) for d in self._deps]
114162

115-
return _invoke_method(info["call"], _args, path=_repo.root_dir)
163+
with self.repo.state:
164+
for out in outs:
165+
self.repo.cloud.pull(out.get_used_cache())
166+
out.checkout()
116167

117168

118-
def _get_object_from_summon_file(name, path):
169+
def _get_object_spec(name, path):
119170
"""
120171
Given a summonable object's name, search for it on the given file
121172
and return its description.
122173
"""
123174
try:
124-
with open(path, "r") as fobj:
125-
content = SUMMON_SCHEMA(ruamel.yaml.safe_load(fobj.read()))
175+
with builtin_open(path, "r") as fobj:
176+
content = SUMMON_FILE_SCHEMA(ruamel.yaml.safe_load(fobj.read()))
126177
objects = [x for x in content["objects"] if x["name"] == name]
127178

128179
if not objects:
@@ -134,34 +185,20 @@ def _get_object_from_summon_file(name, path):
134185

135186
return objects[0]
136187

137-
except FileMissingError:
138-
raise SummonError("Summon file not found")
188+
except FileNotFoundError as exc:
189+
raise SummonError("Summon file not found") from exc
139190
except ruamel.yaml.YAMLError as exc:
140191
raise SummonError("Failed to parse summon file") from exc
141192
except Invalid as exc:
142-
raise SummonError(str(exc))
143-
144-
145-
def _pull_dependencies(repo, deps):
146-
if not deps:
147-
return
148-
149-
outs = [repo.find_out_by_relpath(dep) for dep in deps]
150-
151-
with repo.state:
152-
for out in outs:
153-
repo.cloud.pull(out.get_used_cache())
154-
out.checkout()
193+
raise SummonError(str(exc)) from exc
155194

156195

196+
@wrap_with(threading.Lock())
157197
def _invoke_method(call, args, path):
158198
# XXX: Some issues with this approach:
159-
# * Not thread safe
160199
# * Import will pollute sys.modules
161-
# * Weird errors if there is a name clash within sys.modules
162-
163-
# XXX: sys.path manipulation is "theoretically" not needed
164-
# but tests are failing for an unknown reason.
200+
# * sys.path manipulation is "theoretically" not needed,
201+
# but tests are failing for an unknown reason.
165202
cwd = os.getcwd()
166203

167204
try:

0 commit comments

Comments
 (0)