From b938fc9fc92ecd35546c5ff921eab543f5d2966e Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 18 Jul 2019 22:40:40 -0700 Subject: [PATCH] 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. --- .../base/tests/test_traits_extension.py | 152 ++++++++++++++++++ nipype/interfaces/base/traits_extension.py | 123 ++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 nipype/interfaces/base/tests/test_traits_extension.py diff --git a/nipype/interfaces/base/tests/test_traits_extension.py b/nipype/interfaces/base/tests/test_traits_extension.py new file mode 100644 index 0000000000..4a2b884921 --- /dev/null +++ b/nipype/interfaces/base/tests/test_traits_extension.py @@ -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']]] diff --git a/nipype/interfaces/base/traits_extension.py b/nipype/interfaces/base/traits_extension.py index de215beb96..e0a8705ad1 100644 --- a/nipype/interfaces/base/traits_extension.py +++ b/nipype/interfaces/base/traits_extension.py @@ -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 @@ -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.""" @@ -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