Skip to content

Commit 0013877

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 30b87cf commit 0013877

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
@@ -307,6 +308,11 @@ def validate(self, objekt, name, value, return_pathlike=False):
307308
return value
308309

309310

311+
# Patch in traits these two new
312+
traits.File = File
313+
traits.Directory = Directory
314+
315+
310316
class ImageFile(File):
311317
"""Defines a trait whose value must be a known neuroimaging file."""
312318

@@ -468,3 +474,120 @@ class InputMultiObject(MultiObject):
468474

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

0 commit comments

Comments
 (0)