Skip to content

Commit 5d1e8e7

Browse files
committed
feat(models): reference context for relative paths
(closes #452)
1 parent eae30f4 commit 5d1e8e7

File tree

10 files changed

+148
-49
lines changed

10 files changed

+148
-49
lines changed

renku/api/datasets.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ def datasets(self):
5656
"""Return mapping from path to dataset."""
5757
result = {}
5858
for path in self.renku_datasets_path.rglob(self.METADATA):
59-
with path.open('r') as fp:
60-
result[path] = Dataset.from_jsonld(yaml.load(fp))
59+
result[path] = Dataset.from_yaml(path)
6160
return result
6261

6362
@contextmanager
@@ -80,7 +79,7 @@ def with_dataset(self, name=None):
8079
if path.exists():
8180
with path.open('r') as f:
8281
source = yaml.load(f) or {}
83-
dataset = Dataset.from_jsonld(source)
82+
dataset = Dataset.from_jsonld(source, __reference__=path)
8483

8584
if dataset is None:
8685
source = {}

renku/api/repository.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,10 @@ def with_metadata(self):
256256
if self.renku_metadata_path.exists():
257257
with metadata_path.open('r') as f:
258258
source = yaml.load(f) or {}
259-
metadata = Project.from_jsonld(source)
260259
else:
261260
source = {}
262-
metadata = Project()
261+
262+
metadata = Project.from_jsonld(source, __reference__=metadata_path)
263263

264264
yield metadata
265265

renku/cli/_format/graph.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ def color(p):
280280

281281
def makefile(graph):
282282
"""Format graph as Makefile."""
283-
from renku._compat import Path
284283
from renku.models.provenance.activities import ProcessRun, WorkflowRun
285284

286285
for activity in graph.activities.values():
@@ -293,14 +292,11 @@ def makefile(graph):
293292

294293
for step in steps:
295294
click.echo(' '.join(step.outputs) + ': ' + ' '.join(step.inputs))
296-
297295
tool = step.process
298-
basedir = Path(step.path).parent
299-
300296
click.echo(
301297
'\t@' + ' '.join(tool.to_argv()) + ' ' + ' '.join(
302298
tool.STD_STREAMS_REPR[key] + ' ' + str(path)
303-
for key, path in tool._std_streams(basedir=basedir).items()
299+
for key, path in tool._std_streams().items()
304300
)
305301
)
306302

renku/models/_jsonld.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from pyld import jsonld as ld
3030

3131
from renku._compat import Path
32+
from renku.models._locals import ReferenceMixin, with_reference
3233

3334
KEY = '__json_ld'
3435
KEY_CLS = '__json_ld_cls'
@@ -62,7 +63,10 @@ def wrap(cls):
6263
jsonld_cls = attr.s(cls, **attrs_kwargs)
6364

6465
if not issubclass(jsonld_cls, JSONLDMixin):
65-
jsonld_cls = make_type(cls.__name__, (jsonld_cls, JSONLDMixin), {})
66+
jsonld_cls = attr.s(
67+
make_type(cls.__name__, (jsonld_cls, JSONLDMixin), {}),
68+
**attrs_kwargs
69+
)
6670

6771
# Merge types
6872
for subcls in jsonld_cls.mro():
@@ -291,13 +295,13 @@ def convert_value(v):
291295
return rv
292296

293297

294-
class JSONLDMixin(object):
298+
class JSONLDMixin(ReferenceMixin):
295299
"""Mixin for loading a JSON-LD data."""
296300

297301
__type_registry__ = {}
298302

299303
@classmethod
300-
def from_jsonld(cls, data):
304+
def from_jsonld(cls, data, __reference__=None):
301305
"""Instantiate a JSON-LD class from data."""
302306
if isinstance(data, cls):
303307
return data
@@ -328,10 +332,33 @@ def from_jsonld(cls, data):
328332
# assert compacted['@type'] == cls._jsonld_type, '@type must be equal'
329333
# TODO update self(not cls)._jsonld_context with data['@context']
330334
fields = cls._jsonld_fields
331-
return cls(
332-
**{k.lstrip('_'): v
333-
for k, v in compacted.items() if k in fields}
334-
)
335+
336+
if __reference__:
337+
with with_reference(__reference__):
338+
self = cls(
339+
**{
340+
k.lstrip('_'): v
341+
for k, v in compacted.items() if k in fields
342+
}
343+
)
344+
else:
345+
self = cls(
346+
**{
347+
k.lstrip('_'): v
348+
for k, v in compacted.items() if k in fields
349+
}
350+
)
351+
return self
352+
353+
@classmethod
354+
def from_yaml(cls, path):
355+
"""Return an instance from a YAML file."""
356+
import yaml
357+
358+
with path.open(mode='r') as fp:
359+
self = cls.from_jsonld(yaml.load(fp) or {}, __reference__=path)
360+
361+
return self
335362

336363

337364
s = attrs

renku/models/_locals.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2019 - Swiss Data Science Center (SDSC)
4+
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
5+
# Eidgenössische Technische Hochschule Zürich (ETHZ).
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
"""Manage local contexts."""
19+
20+
from contextlib import contextmanager
21+
22+
import attr
23+
from werkzeug.local import LocalStack
24+
25+
_current_reference = LocalStack()
26+
27+
current_reference = _current_reference()
28+
29+
30+
@contextmanager
31+
def with_reference(path):
32+
"""Manage reference stack."""
33+
_current_reference.push(path)
34+
yield
35+
if path != _current_reference.pop():
36+
raise RuntimeError('current_reference has been modified')
37+
38+
39+
def has_reference():
40+
"""Check if the current reference is bounded."""
41+
return _current_reference.top is not None
42+
43+
44+
@attr.s(cmp=False)
45+
class ReferenceMixin:
46+
"""Define an automatic ``__reference__`` attribute."""
47+
48+
__reference__ = attr.ib(init=False, kw_only=True, repr=False)
49+
50+
@__reference__.default
51+
def default_reference(self):
52+
"""Create a default reference path."""
53+
return current_reference._get_current_object() if has_reference(
54+
) else None

renku/models/cwl/_ascwl.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from attr._make import fields
2626

2727
from renku._compat import Path
28+
from renku.models._locals import ReferenceMixin, with_reference
2829

2930

3031
class CWLType(type):
@@ -40,15 +41,34 @@ def __init__(cls, name, bases, namespace):
4041
cls.registry[name] = cls
4142

4243

43-
class CWLClass(object, metaclass=CWLType):
44+
class CWLClass(ReferenceMixin, metaclass=CWLType):
4445
"""Include ``class`` field in serialized object."""
4546

4647
@classmethod
47-
def from_cwl(cls, data):
48+
def from_cwl(cls, data, __reference__=None):
4849
"""Return an instance from CWL data."""
4950
class_name = data.get('class', None)
5051
cls = cls.registry.get(class_name, cls)
51-
return cls(**{k: v for k, v in iteritems(data) if k != 'class'})
52+
53+
if __reference__:
54+
with with_reference(__reference__):
55+
self = cls(
56+
**{k: v
57+
for k, v in iteritems(data) if k != 'class'}
58+
)
59+
else:
60+
self = cls(**{k: v for k, v in iteritems(data) if k != 'class'})
61+
return self
62+
63+
@classmethod
64+
def from_yaml(cls, path):
65+
"""Return an instance from a YAML file."""
66+
import yaml
67+
68+
with path.open(mode='r') as fp:
69+
self = cls.from_cwl(yaml.load(fp), __reference__=path)
70+
71+
return self
5272

5373

5474
def mapped(cls, key='id', **kwargs):
@@ -114,6 +134,9 @@ def convert_value(v):
114134
return v
115135

116136
for a in attrs:
137+
if a.name.startswith('__'):
138+
continue
139+
117140
a_name = a.name.rstrip('_')
118141
v = getattr(inst, a.name)
119142
if filter is not None and not filter(a, v):

renku/models/cwl/command_line_tool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def to_argv(self, job=None):
142142
if i.inputBinding:
143143
args.append((i.inputBinding.position, i))
144144

145-
for p, v in sorted(args):
145+
for _, v in sorted(args):
146146
argv.extend(v.to_argv())
147147

148148
return argv

renku/models/cwl/parameter.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright 2018 - Swiss Data Science Center (SDSC)
3+
# Copyright 2018-2019 - Swiss Data Science Center (SDSC)
44
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
55
# Eidgenössische Technische Hochschule Zürich (ETHZ).
66
#
@@ -119,10 +119,11 @@ def from_cwl(cls, data):
119119
data = {'type': data}
120120
return cls(**data)
121121

122-
def to_argv(self):
122+
def to_argv(self, **kwargs):
123123
"""Format command input parameter as shell argument."""
124-
return self.inputBinding.to_argv(default=self.default
125-
) if self.inputBinding else []
124+
return self.inputBinding.to_argv(
125+
default=self.default, **kwargs
126+
) if self.inputBinding else []
126127

127128

128129
@attr.s

renku/models/cwl/types.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright 2018 - Swiss Data Science Center (SDSC)
3+
# Copyright 2018-2019 - Swiss Data Science Center (SDSC)
44
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
55
# Eidgenössische Technische Hochschule Zürich (ETHZ).
66
#
@@ -27,35 +27,34 @@
2727
from ._ascwl import CWLClass, ascwl
2828

2929

30-
@attr.s
31-
class File(CWLClass):
32-
"""Represent a file."""
33-
34-
path = attr.ib(converter=Path)
30+
class PathFormatterMixin:
31+
"""Format path property."""
3532

3633
def __str__(self):
3734
"""Simple conversion to string."""
38-
# TODO refactor to use `basedir`
35+
reference = self.__reference__
36+
if reference:
37+
return str(os.path.normpath(str(reference.parent / self.path)))
3938
return os.path.relpath(
4039
os.path.realpath(str(self.path)), os.path.realpath(os.getcwd())
4140
)
4241

4342

4443
@attr.s
45-
class Directory(CWLClass):
44+
class File(CWLClass, PathFormatterMixin):
45+
"""Represent a file."""
46+
47+
path = attr.ib(converter=Path)
48+
49+
50+
@attr.s
51+
class Directory(CWLClass, PathFormatterMixin):
4652
"""Represent a directory."""
4753

4854
# TODO add validation to allow only directories
4955
path = attr.ib(default=None)
5056
listing = attr.ib(default=attr.Factory(list))
5157

52-
def __str__(self):
53-
"""Simple conversion to string."""
54-
# TODO refactor to use `basedir`
55-
return os.path.relpath(
56-
os.path.realpath(str(self.path)), os.path.realpath(os.getcwd())
57-
)
58-
5958

6059
DIRECTORY_EXPRESSION = '$({0})'.format(
6160
json.dumps(ascwl(Directory(), filter=lambda _, x: x is not None))

renku/models/provenance/activities.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright 2018 - Swiss Data Science Center (SDSC)
3+
# Copyright 2018-2019 - Swiss Data Science Center (SDSC)
44
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
55
# Eidgenössische Technische Hochschule Zürich (ETHZ).
66
#
@@ -21,9 +21,9 @@
2121
from collections import OrderedDict
2222

2323
import attr
24-
import yaml
2524
from git import NULL_TREE
2625

26+
from renku._compat import Path
2727
from renku.models import _jsonld as jsonld
2828
from renku.models.cwl import WORKFLOW_STEP_RUN_TYPES
2929
from renku.models.cwl._ascwl import CWLClass
@@ -408,8 +408,6 @@ class WorkflowRun(ProcessRun):
408408
@children.default
409409
def default_children(self):
410410
"""Load children from process."""
411-
import yaml
412-
413411
basedir = os.path.dirname(self.path) if self.path is not None else None
414412

415413
def _load(step):
@@ -418,12 +416,12 @@ def _load(step):
418416
return step.run
419417

420418
if self.commit:
419+
import yaml
421420
data = (self.commit.tree / basedir /
422421
step.run).data_stream.read()
423-
else:
424-
with step.run.open('r') as f:
425-
data = f.read()
426-
return CWLClass.from_cwl(yaml.load(data))
422+
return CWLClass.from_cwl(yaml.load(data))
423+
424+
return CWLClass.from_yaml(step.run)
427425

428426
return {step.id: _load(step) for step in self.process.steps}
429427

@@ -593,8 +591,10 @@ def from_git_commit(commit, client, path=None):
593591
path = file_
594592

595593
if path:
594+
import yaml
595+
596596
data = (commit.tree / path).data_stream.read()
597-
process = CWLClass.from_cwl(yaml.load(data))
597+
process = CWLClass.from_cwl(yaml.load(data), __reference__=Path(path))
598598

599599
return process.create_run(
600600
commit=commit,

0 commit comments

Comments
 (0)