Skip to content

Commit b938fc9

Browse files
committed
ENH: Add resolve/rebase BasePath traits methods & tests
Two new methods ``resolve_path_traits`` and ``rebase_path_traits`` are being included. They take trait instances from a spec (selected via ``spec.trait('traitname')``, the value and a base path. These two functions will be usefull to progress towards #2944.
1 parent 3454c9a commit b938fc9

File tree

2 files changed

+275
-0
lines changed

2 files changed

+275
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
from __future__ import print_function, unicode_literals
5+
6+
from ... import base as nib
7+
from ..traits_extension import rebase_path_traits, resolve_path_traits, Path
8+
9+
10+
class _test_spec(nib.TraitedSpec):
11+
a = nib.traits.File()
12+
b = nib.traits.Tuple(nib.File(),
13+
nib.File())
14+
c = nib.traits.List(nib.File())
15+
d = nib.traits.Either(nib.File(), nib.traits.Float())
16+
e = nib.OutputMultiObject(nib.File())
17+
f = nib.traits.Dict(nib.Str, nib.File())
18+
g = nib.traits.Either(nib.File, nib.Str)
19+
h = nib.Str
20+
ee = nib.OutputMultiObject(nib.Str)
21+
22+
23+
def test_rebase_path_traits():
24+
"""Check rebase_path_traits."""
25+
spec = _test_spec()
26+
27+
a = rebase_path_traits(
28+
spec.trait('a'), '/some/path/f1.txt', '/some/path')
29+
assert '%s' % a == 'f1.txt'
30+
31+
b = rebase_path_traits(
32+
spec.trait('b'), ('/some/path/f1.txt', '/some/path/f2.txt'), '/some/path')
33+
assert b == (Path('f1.txt'), Path('f2.txt'))
34+
35+
c = rebase_path_traits(
36+
spec.trait('c'), ['/some/path/f1.txt', '/some/path/f2.txt', '/some/path/f3.txt'],
37+
'/some/path')
38+
assert c == [Path('f1.txt'), Path('f2.txt'), Path('f3.txt')]
39+
40+
d = rebase_path_traits(
41+
spec.trait('d'), 2.0, '/some/path')
42+
assert d == 2.0
43+
44+
d = rebase_path_traits(
45+
spec.trait('d'), '/some/path/either.txt', '/some/path')
46+
assert '%s' % d == 'either.txt'
47+
48+
e = rebase_path_traits(
49+
spec.trait('e'), ['/some/path/f1.txt', '/some/path/f2.txt', '/some/path/f3.txt'],
50+
'/some/path')
51+
assert e == [Path('f1.txt'), Path('f2.txt'), Path('f3.txt')]
52+
53+
e = rebase_path_traits(
54+
spec.trait('e'), [['/some/path/f1.txt', '/some/path/f2.txt'], [['/some/path/f3.txt']]],
55+
'/some/path')
56+
assert e == [[Path('f1.txt'), Path('f2.txt')], [[Path('f3.txt')]]]
57+
58+
f = rebase_path_traits(
59+
spec.trait('f'), {'1': '/some/path/f1.txt'}, '/some/path')
60+
assert f == {'1': Path('f1.txt')}
61+
62+
g = rebase_path_traits(
63+
spec.trait('g'), 'some/path/either.txt', '/some/path')
64+
assert '%s' % g == 'some/path/either.txt'
65+
66+
g = rebase_path_traits(
67+
spec.trait('g'), '/some/path/either.txt', '/some')
68+
assert '%s' % g == 'path/either.txt'
69+
70+
g = rebase_path_traits(spec.trait('g'), 'string', '/some')
71+
assert '%s' % g == 'string'
72+
73+
g = rebase_path_traits(spec.trait('g'), '2', '/some/path')
74+
assert g == '2' # You dont want this one to be a Path
75+
76+
h = rebase_path_traits(spec.trait('h'), '2', '/some/path')
77+
assert h == '2'
78+
79+
ee = rebase_path_traits(
80+
spec.trait('ee'), [['/some/path/f1.txt', '/some/path/f2.txt'], [['/some/path/f3.txt']]],
81+
'/some/path')
82+
assert ee == [['/some/path/f1.txt', '/some/path/f2.txt'], [['/some/path/f3.txt']]]
83+
84+
85+
def test_resolve_path_traits():
86+
"""Check resolve_path_traits."""
87+
spec = _test_spec()
88+
89+
a = resolve_path_traits(
90+
spec.trait('a'), 'f1.txt', '/some/path')
91+
assert a == Path('/some/path/f1.txt')
92+
93+
b = resolve_path_traits(
94+
spec.trait('b'), ('f1.txt', 'f2.txt'), '/some/path')
95+
assert b == (Path('/some/path/f1.txt'), Path('/some/path/f2.txt'))
96+
97+
c = resolve_path_traits(
98+
spec.trait('c'), ['f1.txt', 'f2.txt', 'f3.txt'],
99+
'/some/path')
100+
assert c == [Path('/some/path/f1.txt'), Path('/some/path/f2.txt'), Path('/some/path/f3.txt')]
101+
102+
d = resolve_path_traits(
103+
spec.trait('d'), 2.0, '/some/path')
104+
assert d == 2.0
105+
106+
d = resolve_path_traits(
107+
spec.trait('d'), 'either.txt', '/some/path')
108+
assert '%s' % d == '/some/path/either.txt'
109+
110+
e = resolve_path_traits(
111+
spec.trait('e'), ['f1.txt', 'f2.txt', 'f3.txt'],
112+
'/some/path')
113+
assert e == [Path('/some/path/f1.txt'), Path('/some/path/f2.txt'), Path('/some/path/f3.txt')]
114+
115+
e = resolve_path_traits(
116+
spec.trait('e'), [['f1.txt', 'f2.txt'], [['f3.txt']]],
117+
'/some/path')
118+
assert e == [[Path('/some/path/f1.txt'), Path('/some/path/f2.txt')],
119+
[[Path('/some/path/f3.txt')]]]
120+
121+
f = resolve_path_traits(
122+
spec.trait('f'), {'1': 'path/f1.txt'}, '/some')
123+
assert f == {'1': Path('/some/path/f1.txt')}
124+
125+
g = resolve_path_traits(
126+
spec.trait('g'), '/either.txt', '/some/path')
127+
assert g == Path('/either.txt')
128+
129+
# This is a problematic case, it is impossible to know whether this
130+
# was meant to be a string or a file.
131+
# Commented out because in this implementation, strings take precedence
132+
# g = resolve_path_traits(
133+
# spec.trait('g'), 'path/either.txt', '/some')
134+
# assert g == Path('/some/path/either.txt')
135+
136+
# This is a problematic case, it is impossible to know whether this
137+
# was meant to be a string or a file.
138+
g = resolve_path_traits(spec.trait('g'), 'string', '/some')
139+
assert g == 'string'
140+
141+
# This is a problematic case, it is impossible to know whether this
142+
# was meant to be a string or a file.
143+
g = resolve_path_traits(spec.trait('g'), '2', '/some/path')
144+
assert g == '2' # You dont want this one to be a Path
145+
146+
h = resolve_path_traits(spec.trait('h'), '2', '/some/path')
147+
assert h == '2'
148+
149+
ee = resolve_path_traits(
150+
spec.trait('ee'), [['f1.txt', 'f2.txt'], [['f3.txt']]],
151+
'/some/path')
152+
assert ee == [['f1.txt', 'f2.txt'], [['f3.txt']]]

nipype/interfaces/base/traits_extension.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import traits.api as traits
3131
from traits.trait_handlers import TraitType, NoDefaultSpecified
3232
from traits.trait_base import _Undefined
33+
from traits.traits import _TraitMaker, trait_from
3334

3435
from traits.api import Unicode
3536
from future import standard_library
@@ -304,6 +305,11 @@ def validate(self, objekt, name, value, return_pathlike=False):
304305
return value
305306

306307

308+
# Patch in traits these two new
309+
traits.File = File
310+
traits.Directory = Directory
311+
312+
307313
class ImageFile(File):
308314
"""Defines a trait whose value must be a known neuroimaging file."""
309315

@@ -465,3 +471,120 @@ class InputMultiObject(MultiObject):
465471

466472
InputMultiPath = InputMultiObject
467473
OutputMultiPath = OutputMultiObject
474+
475+
476+
class Tuple(traits.BaseTuple):
477+
"""Defines a new type of Tuple trait that reports inner types."""
478+
479+
def init_fast_validator(self, *args):
480+
"""Set up the C-level fast validator."""
481+
super(Tuple, self).init_fast_validator(*args)
482+
self.fast_validate = args
483+
484+
def inner_traits(self):
485+
"""Return the *inner trait* (or traits) for this trait."""
486+
return self.types
487+
488+
489+
class PatchedEither(TraitType):
490+
"""Defines a trait whose value can be any of of a specified list of traits."""
491+
492+
def __init__(self, *traits, **metadata):
493+
"""Create a trait whose value can be any of of a specified list of traits."""
494+
metadata['alternatives'] = tuple(trait_from(t) for t in traits)
495+
self.trait_maker = _TraitMaker(
496+
metadata.pop("default", None), *traits, **metadata)
497+
498+
def as_ctrait(self):
499+
"""Return a CTrait corresponding to the trait defined by this class."""
500+
return self.trait_maker.as_ctrait()
501+
502+
503+
traits.Tuple = Tuple
504+
traits.Either = PatchedEither
505+
506+
507+
def _rebase_path(value, cwd):
508+
if isinstance(value, list):
509+
return [_rebase_path(v, cwd) for v in value]
510+
511+
try:
512+
value = Path(value)
513+
except TypeError:
514+
pass
515+
else:
516+
try:
517+
value = Path(value).relative_to(cwd)
518+
except ValueError:
519+
pass
520+
return value
521+
522+
523+
def rebase_path_traits(thistrait, value, cwd):
524+
"""Rebase a BasePath-derived trait given an interface spec."""
525+
if thistrait.is_trait_type(BasePath):
526+
value = _rebase_path(value, cwd)
527+
elif thistrait.is_trait_type(traits.List):
528+
innertrait, = thistrait.inner_traits
529+
if not isinstance(value, (list, tuple)):
530+
value = rebase_path_traits(innertrait, value, cwd)
531+
else:
532+
value = [rebase_path_traits(innertrait, v, cwd)
533+
for v in value]
534+
elif thistrait.is_trait_type(traits.Dict):
535+
_, innertrait = thistrait.inner_traits
536+
value = {k: rebase_path_traits(innertrait, v, cwd)
537+
for k, v in value.items()}
538+
elif thistrait.is_trait_type(Tuple):
539+
value = tuple([rebase_path_traits(subtrait, v, cwd)
540+
for subtrait, v in zip(thistrait.inner_traits, value)])
541+
elif thistrait.alternatives:
542+
is_str = [f.is_trait_type((traits.String, traits.BaseStr, traits.BaseBytes, Str))
543+
for f in thistrait.alternatives]
544+
if any(is_str) and isinstance(value, (bytes, str)) and not value.startswith('/'):
545+
return value
546+
for subtrait in thistrait.alternatives:
547+
value = rebase_path_traits(subtrait, value, cwd)
548+
return value
549+
550+
551+
def _resolve_path(value, cwd):
552+
if isinstance(value, list):
553+
return [_resolve_path(v, cwd) for v in value]
554+
555+
try:
556+
value = Path(value)
557+
except TypeError:
558+
pass
559+
else:
560+
if not value.is_absolute():
561+
value = Path(cwd) / value
562+
return value
563+
564+
565+
def resolve_path_traits(thistrait, value, cwd):
566+
"""Resolve a BasePath-derived trait given an interface spec."""
567+
if thistrait.is_trait_type(BasePath):
568+
value = _resolve_path(value, cwd)
569+
elif thistrait.is_trait_type(traits.List):
570+
innertrait, = thistrait.inner_traits
571+
if not isinstance(value, (list, tuple)):
572+
value = resolve_path_traits(innertrait, value, cwd)
573+
else:
574+
value = [resolve_path_traits(innertrait, v, cwd)
575+
for v in value]
576+
elif thistrait.is_trait_type(traits.Dict):
577+
_, innertrait = thistrait.inner_traits
578+
value = {k: resolve_path_traits(innertrait, v, cwd)
579+
for k, v in value.items()}
580+
elif thistrait.is_trait_type(Tuple):
581+
value = tuple([resolve_path_traits(subtrait, v, cwd)
582+
for subtrait, v in zip(thistrait.inner_traits, value)])
583+
elif thistrait.alternatives:
584+
is_str = [f.is_trait_type((traits.String, traits.BaseStr, traits.BaseBytes, Str))
585+
for f in thistrait.alternatives]
586+
if any(is_str) and isinstance(value, (bytes, str)) and not value.startswith('/'):
587+
return value
588+
for subtrait in thistrait.alternatives:
589+
value = resolve_path_traits(subtrait, value, cwd)
590+
return value

0 commit comments

Comments
 (0)