Skip to content

Commit

Permalink
ENH: Add resolve/rebase BasePath traits methods & tests
Browse files Browse the repository at this point in the history
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 nipy#2944.
  • Loading branch information
oesteban committed Jul 19, 2019
1 parent 3454c9a commit b938fc9
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 0 deletions.
152 changes: 152 additions & 0 deletions nipype/interfaces/base/tests/test_traits_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
from __future__ import print_function, unicode_literals

from ... import base as nib
from ..traits_extension import rebase_path_traits, resolve_path_traits, Path


class _test_spec(nib.TraitedSpec):
a = nib.traits.File()
b = nib.traits.Tuple(nib.File(),
nib.File())
c = nib.traits.List(nib.File())
d = nib.traits.Either(nib.File(), nib.traits.Float())
e = nib.OutputMultiObject(nib.File())
f = nib.traits.Dict(nib.Str, nib.File())
g = nib.traits.Either(nib.File, nib.Str)
h = nib.Str
ee = nib.OutputMultiObject(nib.Str)


def test_rebase_path_traits():
"""Check rebase_path_traits."""
spec = _test_spec()

a = rebase_path_traits(
spec.trait('a'), '/some/path/f1.txt', '/some/path')
assert '%s' % a == 'f1.txt'

b = rebase_path_traits(
spec.trait('b'), ('/some/path/f1.txt', '/some/path/f2.txt'), '/some/path')
assert b == (Path('f1.txt'), Path('f2.txt'))

c = rebase_path_traits(
spec.trait('c'), ['/some/path/f1.txt', '/some/path/f2.txt', '/some/path/f3.txt'],
'/some/path')
assert c == [Path('f1.txt'), Path('f2.txt'), Path('f3.txt')]

d = rebase_path_traits(
spec.trait('d'), 2.0, '/some/path')
assert d == 2.0

d = rebase_path_traits(
spec.trait('d'), '/some/path/either.txt', '/some/path')
assert '%s' % d == 'either.txt'

e = rebase_path_traits(
spec.trait('e'), ['/some/path/f1.txt', '/some/path/f2.txt', '/some/path/f3.txt'],
'/some/path')
assert e == [Path('f1.txt'), Path('f2.txt'), Path('f3.txt')]

e = rebase_path_traits(
spec.trait('e'), [['/some/path/f1.txt', '/some/path/f2.txt'], [['/some/path/f3.txt']]],
'/some/path')
assert e == [[Path('f1.txt'), Path('f2.txt')], [[Path('f3.txt')]]]

f = rebase_path_traits(
spec.trait('f'), {'1': '/some/path/f1.txt'}, '/some/path')
assert f == {'1': Path('f1.txt')}

g = rebase_path_traits(
spec.trait('g'), 'some/path/either.txt', '/some/path')
assert '%s' % g == 'some/path/either.txt'

g = rebase_path_traits(
spec.trait('g'), '/some/path/either.txt', '/some')
assert '%s' % g == 'path/either.txt'

g = rebase_path_traits(spec.trait('g'), 'string', '/some')
assert '%s' % g == 'string'

g = rebase_path_traits(spec.trait('g'), '2', '/some/path')
assert g == '2' # You dont want this one to be a Path

h = rebase_path_traits(spec.trait('h'), '2', '/some/path')
assert h == '2'

ee = rebase_path_traits(
spec.trait('ee'), [['/some/path/f1.txt', '/some/path/f2.txt'], [['/some/path/f3.txt']]],
'/some/path')
assert ee == [['/some/path/f1.txt', '/some/path/f2.txt'], [['/some/path/f3.txt']]]


def test_resolve_path_traits():
"""Check resolve_path_traits."""
spec = _test_spec()

a = resolve_path_traits(
spec.trait('a'), 'f1.txt', '/some/path')
assert a == Path('/some/path/f1.txt')

b = resolve_path_traits(
spec.trait('b'), ('f1.txt', 'f2.txt'), '/some/path')
assert b == (Path('/some/path/f1.txt'), Path('/some/path/f2.txt'))

c = resolve_path_traits(
spec.trait('c'), ['f1.txt', 'f2.txt', 'f3.txt'],
'/some/path')
assert c == [Path('/some/path/f1.txt'), Path('/some/path/f2.txt'), Path('/some/path/f3.txt')]

d = resolve_path_traits(
spec.trait('d'), 2.0, '/some/path')
assert d == 2.0

d = resolve_path_traits(
spec.trait('d'), 'either.txt', '/some/path')
assert '%s' % d == '/some/path/either.txt'

e = resolve_path_traits(
spec.trait('e'), ['f1.txt', 'f2.txt', 'f3.txt'],
'/some/path')
assert e == [Path('/some/path/f1.txt'), Path('/some/path/f2.txt'), Path('/some/path/f3.txt')]

e = resolve_path_traits(
spec.trait('e'), [['f1.txt', 'f2.txt'], [['f3.txt']]],
'/some/path')
assert e == [[Path('/some/path/f1.txt'), Path('/some/path/f2.txt')],
[[Path('/some/path/f3.txt')]]]

f = resolve_path_traits(
spec.trait('f'), {'1': 'path/f1.txt'}, '/some')
assert f == {'1': Path('/some/path/f1.txt')}

g = resolve_path_traits(
spec.trait('g'), '/either.txt', '/some/path')
assert g == Path('/either.txt')

# This is a problematic case, it is impossible to know whether this
# was meant to be a string or a file.
# Commented out because in this implementation, strings take precedence
# g = resolve_path_traits(
# spec.trait('g'), 'path/either.txt', '/some')
# assert g == Path('/some/path/either.txt')

# This is a problematic case, it is impossible to know whether this
# was meant to be a string or a file.
g = resolve_path_traits(spec.trait('g'), 'string', '/some')
assert g == 'string'

# This is a problematic case, it is impossible to know whether this
# was meant to be a string or a file.
g = resolve_path_traits(spec.trait('g'), '2', '/some/path')
assert g == '2' # You dont want this one to be a Path

h = resolve_path_traits(spec.trait('h'), '2', '/some/path')
assert h == '2'

ee = resolve_path_traits(
spec.trait('ee'), [['f1.txt', 'f2.txt'], [['f3.txt']]],
'/some/path')
assert ee == [['f1.txt', 'f2.txt'], [['f3.txt']]]
123 changes: 123 additions & 0 deletions nipype/interfaces/base/traits_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import traits.api as traits
from traits.trait_handlers import TraitType, NoDefaultSpecified
from traits.trait_base import _Undefined
from traits.traits import _TraitMaker, trait_from

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


# Patch in traits these two new
traits.File = File
traits.Directory = Directory


class ImageFile(File):
"""Defines a trait whose value must be a known neuroimaging file."""

Expand Down Expand Up @@ -465,3 +471,120 @@ class InputMultiObject(MultiObject):

InputMultiPath = InputMultiObject
OutputMultiPath = OutputMultiObject


class Tuple(traits.BaseTuple):
"""Defines a new type of Tuple trait that reports inner types."""

def init_fast_validator(self, *args):
"""Set up the C-level fast validator."""
super(Tuple, self).init_fast_validator(*args)
self.fast_validate = args

def inner_traits(self):
"""Return the *inner trait* (or traits) for this trait."""
return self.types


class PatchedEither(TraitType):
"""Defines a trait whose value can be any of of a specified list of traits."""

def __init__(self, *traits, **metadata):
"""Create a trait whose value can be any of of a specified list of traits."""
metadata['alternatives'] = tuple(trait_from(t) for t in traits)
self.trait_maker = _TraitMaker(
metadata.pop("default", None), *traits, **metadata)

def as_ctrait(self):
"""Return a CTrait corresponding to the trait defined by this class."""
return self.trait_maker.as_ctrait()


traits.Tuple = Tuple
traits.Either = PatchedEither


def _rebase_path(value, cwd):
if isinstance(value, list):
return [_rebase_path(v, cwd) for v in value]

try:
value = Path(value)
except TypeError:
pass
else:
try:
value = Path(value).relative_to(cwd)
except ValueError:
pass
return value


def rebase_path_traits(thistrait, value, cwd):
"""Rebase a BasePath-derived trait given an interface spec."""
if thistrait.is_trait_type(BasePath):
value = _rebase_path(value, cwd)
elif thistrait.is_trait_type(traits.List):
innertrait, = thistrait.inner_traits
if not isinstance(value, (list, tuple)):
value = rebase_path_traits(innertrait, value, cwd)
else:
value = [rebase_path_traits(innertrait, v, cwd)
for v in value]
elif thistrait.is_trait_type(traits.Dict):
_, innertrait = thistrait.inner_traits
value = {k: rebase_path_traits(innertrait, v, cwd)
for k, v in value.items()}
elif thistrait.is_trait_type(Tuple):
value = tuple([rebase_path_traits(subtrait, v, cwd)
for subtrait, v in zip(thistrait.inner_traits, value)])
elif thistrait.alternatives:
is_str = [f.is_trait_type((traits.String, traits.BaseStr, traits.BaseBytes, Str))
for f in thistrait.alternatives]
if any(is_str) and isinstance(value, (bytes, str)) and not value.startswith('/'):
return value
for subtrait in thistrait.alternatives:
value = rebase_path_traits(subtrait, value, cwd)
return value


def _resolve_path(value, cwd):
if isinstance(value, list):
return [_resolve_path(v, cwd) for v in value]

try:
value = Path(value)
except TypeError:
pass
else:
if not value.is_absolute():
value = Path(cwd) / value
return value


def resolve_path_traits(thistrait, value, cwd):
"""Resolve a BasePath-derived trait given an interface spec."""
if thistrait.is_trait_type(BasePath):
value = _resolve_path(value, cwd)
elif thistrait.is_trait_type(traits.List):
innertrait, = thistrait.inner_traits
if not isinstance(value, (list, tuple)):
value = resolve_path_traits(innertrait, value, cwd)
else:
value = [resolve_path_traits(innertrait, v, cwd)
for v in value]
elif thistrait.is_trait_type(traits.Dict):
_, innertrait = thistrait.inner_traits
value = {k: resolve_path_traits(innertrait, v, cwd)
for k, v in value.items()}
elif thistrait.is_trait_type(Tuple):
value = tuple([resolve_path_traits(subtrait, v, cwd)
for subtrait, v in zip(thistrait.inner_traits, value)])
elif thistrait.alternatives:
is_str = [f.is_trait_type((traits.String, traits.BaseStr, traits.BaseBytes, Str))
for f in thistrait.alternatives]
if any(is_str) and isinstance(value, (bytes, str)) and not value.startswith('/'):
return value
for subtrait in thistrait.alternatives:
value = resolve_path_traits(subtrait, value, cwd)
return value

0 comments on commit b938fc9

Please sign in to comment.