-
Notifications
You must be signed in to change notification settings - Fork 1.2k
summon: first draft with python specific implementation #2971
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
Changes from all commits
3d13318
b921730
789db91
d0344ce
5d66a57
fea3d14
4ce1113
32f35ea
0a54683
1b3c771
29339d2
4273ff4
ee72280
c852d33
addc56c
4ad7cb3
7ece84b
6fd5eb4
542f4c3
7dd340a
1c22cd5
d45f07c
fbb7aeb
1e65c76
7a58901
0658622
75ad14a
4a25612
49264b5
b977471
e8dd235
dad224e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,46 @@ | ||
| import importlib | ||
| import os | ||
| import sys | ||
| import copy | ||
| from contextlib import contextmanager | ||
|
|
||
| try: | ||
| from contextlib import _GeneratorContextManager as GCM | ||
| except ImportError: | ||
| from contextlib import GeneratorContextManager as GCM | ||
|
|
||
| from dvc.utils.compat import urlparse | ||
| from dvc.utils.compat import urlparse, builtin_str | ||
|
|
||
| import ruamel.yaml | ||
| from voluptuous import Schema, Required, Invalid | ||
|
|
||
| from dvc.repo import Repo | ||
| from dvc.exceptions import DvcException, FileMissingError | ||
| from dvc.external_repo import external_repo | ||
|
|
||
|
|
||
| SUMMON_SCHEMA = Schema( | ||
| { | ||
| Required("objects"): [ | ||
| { | ||
| Required("name"): str, | ||
| "meta": dict, | ||
| Required("summon"): { | ||
| Required("type"): "python", | ||
| Required("call"): str, | ||
| "args": dict, | ||
| "deps": [str], | ||
| }, | ||
| } | ||
| ] | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| class SummonError(DvcException): | ||
| pass | ||
|
|
||
|
|
||
| def get_url(path, repo=None, rev=None, remote=None): | ||
| """Returns an url of a resource specified by path in repo""" | ||
| with _make_repo(repo, rev=rev) as _repo: | ||
|
|
@@ -69,3 +99,99 @@ def _make_repo(repo_url, rev=None): | |
| else: | ||
| with external_repo(url=repo_url, rev=rev) as repo: | ||
| yield repo | ||
|
|
||
|
|
||
| def summon(name, repo=None, rev=None, summon_file="dvcsummon.yaml", args=None): | ||
| """Instantiate an object described in the summon file.""" | ||
| with _make_repo(repo, rev=rev) as _repo: | ||
| try: | ||
| path = os.path.join(_repo.root_dir, summon_file) | ||
| obj = _get_object_from_summon_file(name, path) | ||
| info = obj["summon"] | ||
| except SummonError as exc: | ||
| raise SummonError( | ||
| str(exc) + " at '{}' in '{}'".format(summon_file, repo), | ||
| cause=exc.cause, | ||
| ) | ||
|
|
||
| _pull_dependencies(_repo, info.get("deps", [])) | ||
|
|
||
| _args = copy.deepcopy(info.get("args", {})) | ||
| _args.update(args or {}) | ||
|
|
||
| return _invoke_method(info["call"], _args, path=_repo.root_dir) | ||
|
|
||
|
|
||
| def _get_object_from_summon_file(name, path): | ||
| """ | ||
| Given a summonable object's name, search for it on the given file | ||
| and return its description. | ||
| """ | ||
| try: | ||
| with open(path, "r") as fobj: | ||
| content = SUMMON_SCHEMA(ruamel.yaml.safe_load(fobj.read())) | ||
| objects = [x for x in content["objects"] if x["name"] == name] | ||
|
|
||
| if not objects: | ||
| raise SummonError("No object with name '{}'".format(name)) | ||
| elif len(objects) >= 2: | ||
| raise SummonError( | ||
| "More than one object with name '{}'".format(name) | ||
| ) | ||
|
|
||
| return objects[0] | ||
|
|
||
| except FileMissingError: | ||
| raise SummonError("Summon file not found") | ||
| except ruamel.yaml.YAMLError as exc: | ||
| raise SummonError("Failed to parse summon file", exc) | ||
| except Invalid as exc: | ||
| raise SummonError(str(exc)) | ||
|
|
||
|
|
||
| def _pull_dependencies(repo, deps): | ||
| if not deps: | ||
| return | ||
|
|
||
| outs = [repo.find_out_by_relpath(dep) for dep in deps] | ||
|
|
||
| with repo.state: | ||
| for out in outs: | ||
| repo.cloud.pull(out.get_used_cache()) | ||
| out.checkout() | ||
|
|
||
|
|
||
| def _invoke_method(call, args, path): | ||
| # XXX: Some issues with this approach: | ||
| # * Not thread safe | ||
| # * Import will pollute sys.modules | ||
| # * Weird errors if there is a name clash within sys.modules | ||
|
Comment on lines
+165
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no good non complicated way to handle this in Python 2. I will come up with an approach next sprint. Leave as is for now. |
||
|
|
||
| # XXX: sys.path manipulation is "theoretically" not needed | ||
| # but tests are failing for an unknown reason. | ||
| cwd = os.getcwd() | ||
|
|
||
| try: | ||
| os.chdir(path) | ||
This conversation was marked as resolved.
Show resolved
Hide resolved
|
||
| sys.path.insert(0, path) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, don't need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Suor , left a commend right here: #2971 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only it works as long as you have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also might be the reason why random tests failed. |
||
| method = _import_string(call) | ||
| return method(**args) | ||
| finally: | ||
| os.chdir(cwd) | ||
| sys.path.pop(0) | ||
|
|
||
|
|
||
| def _import_string(import_name): | ||
| """Imports an object based on a string. | ||
| Useful to delay import to not load everything on startup. | ||
| Use dotted notaion in `import_name`, e.g. 'dvc.remote.gs.RemoteGS'. | ||
|
|
||
| :return: imported object | ||
| """ | ||
| import_name = builtin_str(import_name) | ||
|
|
||
| if "." in import_name: | ||
| module, obj = import_name.rsplit(".", 1) | ||
| else: | ||
| return importlib.import_module(import_name) | ||
| return getattr(importlib.import_module(module), obj) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either remove
or {}here or a default value to.get()on prev line.