Completed
Push — master ( bb496e...0c9be4 )
by Alexandre M.
52s
created

hansel.Crumb._arg_values()   D

Complexity

Conditions 9

Size

Total Lines 70

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 9
dl 0
loc 70
rs 4.2631

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
"""
5
Crumb class: the smart path model class.
6
"""
7
import re
8
import os.path     as op
9
from   copy        import deepcopy
10
from   collections import OrderedDict, Mapping, Sequence
11
from   functools   import partial
12
from   six         import string_types
13
try:
14
    from pathlib2 import Path
15
except:
16
    from pathlib  import Path
17
18
19
from   .utils  import (list_subpaths,
20
                       fnmatch_filter,
21
                       regex_match_filter,
22
                       )
23
24
#from hansel._utils import deprecated
25
from   ._utils import (_get_path,
26
                       _is_crumb_arg,
27
                       _replace,
28
                       _split_exists,
29
                       _split,
30
                       _touch,
31
                       _arg_params,
32
                       _dict_popitems,
33
                       has_crumbs,
34
                       is_valid,
35
                       _arg_format,
36
                       )
37
38
39
class Crumb(object):
40
    """ The crumb path model class.
41
    Parameters
42
    ----------
43
    crumb_path: str
44
        A file or folder path with crumb arguments. See Examples.
45
46
    ignore_list: sequence of str
47
        A list of `fnmatch` patterns of filenames to be ignored.
48
49
    regex: str
50
        Choices: 'fnmatch', 're' or 're.ignorecase'
51
        If 'fnmatch' will use fnmatch regular expressions to
52
        match any expression you may have in a crumb argument.
53
        If 're' will use re.match.
54
        If 're.ignorecase' will use re.match and pass re.IGNORE_CASE to re.compile.
55
56
    Examples
57
    --------
58
    >>> crumb = Crumb("{base_dir}/raw/{subject_id}/{session_id}/{modality}/{image}")
59
    >>> cr = Crumb(op.join(op.expanduser('~'), '{user_folder}'))
60
    """
61
    # symbols indicating start and end of a crumb argument
62
    _start_end_syms = ('{', '}')
63
    _regex_sym = ':'
64
65
    # specify partial functions from _utils with _arg_start_sym and _arg_end_sym
66
    # everything would be much simpler if I hardcoded these symbols but I still
67
    # feel that this flexibility is nice to have.
68
    _is_crumb_arg = partial(_is_crumb_arg, start_end_syms=_start_end_syms)
69
    _arg_params   = partial(_arg_params,   start_end_syms=_start_end_syms, reg_sym=_regex_sym)
70
    _arg_format   = partial(_arg_format,   start_end_syms=_start_end_syms, reg_sym=_regex_sym)
71
    is_valid      = partial(is_valid,      start_end_syms=_start_end_syms)
72
    has_crumbs    = partial(has_crumbs,    start_end_syms=_start_end_syms)
73
    _split        = partial(_split,        start_end_syms=_start_end_syms)
74
    _touch        = partial(_touch,        start_end_syms=_start_end_syms)
75
    _split_exists = partial(_split_exists, start_end_syms=_start_end_syms)
76
77
    def __init__(self, crumb_path, ignore_list=None, regex='fnmatch'):
78
        self._path    = _get_path(crumb_path)
79
        self._argidx  = OrderedDict()  # in which order the crumb argument appears
80
        self._argval  = {}  # what is the value of the argument in the current path, if any has been set.
81
        self.patterns = {}  # what is the pattern set for the argument, if any. This is left public for the user.
82
        self._re_method = regex
83
        self._re_args   = None
84
85
        if ignore_list is None:
86
            ignore_list = []
87
88
        self._ignore = ignore_list
89
        self._update()
90
91
    @property
92
    def arg_values(self):
93
        """ Return a dict with the arg_names and values of the already replaced crumb arguments."""
94
        return self._argval
95
96
    @property
97
    def path(self):
98
        """Return the current crumb path string."""
99
        return self._path
100
101
    @path.setter
102
    def path(self, value):
103
        """ Set the current crumb path string and updates the internal members.
104
        Parameters
105
        ----------
106
        value: str
107
            A file or folder path with crumb arguments. See Examples in class docstring.
108
        """
109
        self._path = value
110
        self._update()
111
112
    def _open_arg_items(self):
113
        """ Return an iterator to the crumb _argidx items in `self` that have not been replaced yet.
114
        In the same order as they appear in the crumb path.
115
116
        Returns
117
        -------
118
        crumb_args: set of str
119
120
        Note
121
        ----
122
        I know that there is shorter/faster ways to program this but I wanted to maintain the
123
        order of the arguments in argidx in the result of this function.
124
        """
125
        for arg_name, idx in self._argidx.items():
126
            if arg_name not in self._argval:
127
                yield arg_name, idx
128
129
    def has_set(self, arg_name):
130
        """ Return True if the argument `arg_name` has been set to a specific value,
131
        False if it is still a crumb argument."""
132
        return arg_name not in self.open_args()
133
134
    def open_args(self):
135
        """ Return an iterator to the crumb argument names in `self` that have not been replaced yet.
136
        In the same order as they appear in the crumb path.
137
138
        Returns
139
        -------
140
        crumb_args: set of str
141
142
        Note
143
        ----
144
        I know that there is shorter/faster ways to program this but I wanted to maintain the
145
        order of the arguments in argidx in the result of this function.
146
        """
147
        for arg_name, _ in self._open_arg_items():
148
            yield arg_name
149
150
    def all_args(self):
151
        """ Return an iterator to all the crumb argument names in `self`, first the open ones and then the
152
        replaced ones.
153
154
        Returns
155
        -------
156
        crumb_args: set of str
157
        """
158
        for arg_name in self._argidx.keys():
159
            yield arg_name
160
161
    def _check(self):
162
        """ Raise ValueError if the path of the Crumb has errors using `self.is_valid`."""
163
        if not self.is_valid(self._path):
164
            raise ValueError("The current crumb path has errors, got {}.".format(self.path))
165
166
    def _update(self):
167
        """ Clean up, parse the current crumb path and fill the internal
168
        members for functioning."""
169
        self._clean()
170
        self._check()
171
        self._set_argdicts()
172
        self._set_match_function()
173
        self._set_replace_function()
174
175
    def _set_replace_function(self):
176
        """ Set self._replace function as a partial function, adding regex=self.patterns."""
177
        self._replace = partial(_replace,
178
                                start_end_syms=self._start_end_syms,
179
                                regexes=self.patterns)
180
181
    def _set_match_function(self):
182
        """ Update self._match_filter with a regular expression
183
        matching function depending on the value of self._re_method."""
184
        if self._re_method == 'fnmatch':
185
            self._match_filter = fnmatch_filter
186
        elif self._re_method == 're':
187
            self._match_filter = regex_match_filter
188
        elif self._re_method == 're.ignorecase':
189
            self._match_filter = regex_match_filter
190
            self._re_args      = (re.IGNORECASE, )
191
        else:
192
            raise ValueError('Expected regex method value to be "fnmatch", "re" or "re.ignorecase"'
193
                             ', got {}.'.format(self._re_method))
194
195
    def _clean(self):
196
        """ Clean up the private utility members, i.e., _argidx. """
197
        self._argidx = OrderedDict()
198
199
    @classmethod
200
    def copy(cls, crumb):
201
        """ Return a deep copy of the given `crumb`.
202
        Parameters
203
        ----------
204
        crumb: str or Crumb
205
206
        Returns
207
        -------
208
        copy: Crumb
209
        """
210
        if isinstance(crumb, cls):
211
            #nucr = deepcopy(crumb)
212
            nucr = cls(crumb._path, ignore_list=crumb._ignore, regex=crumb._re_method)
213
            nucr._argidx = deepcopy(crumb._argidx)
214
            nucr._argval = deepcopy(crumb._argval)
215
            return nucr
216
        elif isinstance(crumb, string_types):
217
            return cls.from_path(crumb)
218
        else:
219
            raise TypeError("Expected a Crumb or a str to copy, got {}.".format(type(crumb)))
220
221
    def _set_argdicts(self):
222
        """ Initialize the self._argidx dict. It holds arg_name -> index.
223
        The index is the position in the whole `_path.split(op.sep)` where each argument is.
224
        """
225
        fs = self._path_split()
226
        for idx, f in enumerate(fs):
227
            if self._is_crumb_arg(f):
228
                arg_name, arg_regex = self._arg_params(f)
229
                self._argidx[arg_name] = idx
230
231
                if arg_regex is not None:
232
                    self.patterns[arg_name] = arg_regex
233
234
    def _find_arg(self, arg_name):
235
        """ Return the index in the current path of the crumb
236
        argument with name `arg_name`.
237
        """
238
        return self._argidx.get(arg_name, -1)
239
240
    def isabs(self):
241
        """ Return True if the current crumb path has an absolute path, False otherwise.
242
        This means that its path is valid and starts with a `op.sep` character
243
        or hard disk letter.
244
        """
245
        if not self.is_valid(self._path):
246
            raise ValueError("The given crumb path has errors, got {}.".format(self.path))
247
248
        start_sym, _ = self._start_end_syms
249
        subp = self._path.split(start_sym)[0]
250
        return op.isabs(subp)
251
252
    def abspath(self, first_is_basedir=False):
253
        """ Return a copy of `self` with an absolute crumb path.
254
        Add as prefix the absolute path to the current directory if the current
255
        crumb is not absolute.
256
        Parameters
257
        ----------
258
        first_is_basedir: bool
259
            If True and the current crumb path starts with a crumb argument and first_is_basedir,
260
            the first argument will be replaced by the absolute path to the current dir,
261
            otherwise the absolute path to the current dir will be added as a prefix.
262
263
        Returns
264
        -------
265
        abs_crumb: Crumb
266
        """
267
        if not self.is_valid(self._path):
268
            raise ValueError("The given crumb path has errors, got {}.".format(self.path))
269
270
        if self.isabs():
271
            return deepcopy(self)
272
273
        nucr = self.copy(self)
274
        nucr._path = self._abspath(first_is_basedir=first_is_basedir)
275
        return nucr
276
277
    def _path_split(self):
278
        return self._path.split(op.sep)
279
280
    def _abspath(self, first_is_basedir=False):
281
        """ Return the absolute path of the current crumb path.
282
        Parameters
283
        ----------
284
        first_is_basedir: bool
285
            If True and the current crumb path starts with a crumb argument and first_is_basedir,
286
            the first argument will be replaced by the absolute path to the current dir,
287
            otherwise the absolute path to the current dir will be added as a prefix.
288
289
        Returns
290
        -------
291
        abspath: str
292
        """
293
        if not self.has_crumbs(self._path):
294
            return op.abspath(self._path)
295
296
        splt = self._path_split()
297
        path = []
298
        if self._is_crumb_arg(splt[0]):
299
            path.append(op.abspath(op.curdir))
300
301
        if not first_is_basedir:
302
            path.append(splt[0])
303
304
        if splt[1:]:
305
            path.extend(splt[1:])
306
307
        return op.sep.join(path)
308
309
    def split(self):
310
        """ Return a list of sub-strings of the current crumb path where the
311
            path parts are separated from the crumb arguments.
312
313
        Returns
314
        -------
315
        crumbs: list of str
316
        """
317
        return self._split(self._path)
318
319
    @classmethod
320
    def from_path(cls, crumb_path):
321
        """ Create an instance of Crumb out of `crumb_path`.
322
        Parameters
323
        ----------
324
        val: str or Crumb or pathlib.Path
325
326
        Returns
327
        -------
328
        path: Crumb
329
        """
330
        if isinstance(crumb_path, (cls, Path)):
331
            return cls.copy(crumb_path)
332
333
        if isinstance(crumb_path, string_types):
334
            return cls(crumb_path)
335
        else:
336
            raise TypeError("Expected a `val` to be a `str`, got {}.".format(type(crumb_path)))
337
338
    def _last_open_arg(self):
339
        """ Return the name and idx of the last open argument."""
340
        for arg, idx in reversed(list(self._open_arg_items())):
341
            return arg, idx
342
343
    def _first_open_arg(self):
344
        """ Return the name and idx of the first open argument."""
345
        for arg, idx in self._open_arg_items():
346
            return arg, idx
347
348
    def _is_first_open_arg(self, arg_name):
349
        """ Return True if `arg_name` is the first open argument."""
350
        # Take into account that self._argidx is OrderedDict
351
        return arg_name == self._first_open_arg()[0]
352
353
    def _arg_values(self, arg_name, arg_values=None):
354
        """ Return the existing values in the file system for the crumb argument
355
        with name `arg_name`.
356
        The `arg_values` must be a sequence with the tuples with valid values of the dependent
357
        (previous in the path) crumb arguments.
358
        The format of `arg_values` work in such a way that `self._path.format(dict(arg_values[0]))`
359
        would give me a valid path or crumb.
360
        Parameters
361
        ----------
362
        arg_name: str
363
364
        arg_values: list of tuples
365
366
        Returns
367
        -------
368
        vals: list of tuples
369
370
        Raises
371
        ------
372
        ValueError: if `arg_values` is None and `arg_name` is not the
373
        first crumb argument in self._path
374
375
        IOError: if this crosses to any path that is non-existing.
376
        """
377
        if arg_values is None and not self._is_first_open_arg(arg_name):
378
            raise ValueError("Cannot get the list of values for {} if"
379
                             " the previous arguments are not filled"
380
                             " in `paths`.".format(arg_name))
381
382
        aidx = self._find_arg(arg_name)
383
384
        # check if the path is absolute, do it absolute
385
        apath = self._abspath()
386
        splt = apath.split(op.sep)
387
388
        if aidx == len(splt) - 1:  # this means we have to list files too
389
            just_dirs = False
390
        else:  # this means we have to list folders
391
            just_dirs = True
392
393
        vals = []
394
        if arg_values is None:
395
            base = op.sep.join(splt[:aidx])
396
            vals = list_subpaths(base,
397
                                 just_dirs=just_dirs,
398
                                 ignore=self._ignore,
399
                                 pattern=self.patterns.get(arg_name, ''),
400
                                 filter_func=self._match_filter,
401
                                 filter_args=self._re_args)
402
403
            vals = [[(arg_name, val)] for val in vals]
404
        else:
405
            for aval in arg_values:
406
                #  create the part of the crumb path that is already specified
407
                path = self._split(self._replace(self._path,
408
                                                 **dict(aval)))[0]
409
410
                if not op.exists(path):
411
                    continue
412
413
                paths = list_subpaths(path,
414
                                      just_dirs=just_dirs,
415
                                      ignore=self._ignore,
416
                                      pattern=self.patterns.get(arg_name, ''),
417
                                      filter_func=self._match_filter)
418
419
                #  extend `val` tuples with the new list of values for `aval`
420
                vals.extend([aval + [(arg_name, sp)] for sp in paths])
421
422
        return vals
423
424
    def _check_args(self, arg_names, self_args):
425
        """ Raise a ValueError if `self_args` is empty.
426
            Raise a KeyError if `arg_names` is not a subset of `self_args`.
427
        """
428
        if not self_args:
429
            raise ValueError('This Crumb has no remaining arguments: {}.'.format(self.path))
430
431
        if not set(arg_names).issubset(set(self_args)):
432
            raise KeyError("Expected `arg_names` to be a subset of ({}),"
433
                           " got {}.".format(list(self_args), arg_names))
434
435
    def _check_open_args(self, arg_names):
436
        """ Raise a KeyError if any of the arguments in `arg_names` is not a crumb
437
        argument name in `self.path`.
438
        Parameters
439
        ----------
440
        arg_names: sequence of str
441
            Names of crumb arguments
442
443
        Raises
444
        ------
445
        KeyError
446
        """
447
        return self._check_args(arg_names, self_args=self.open_args())
448
449
    def _update_argidx(self, **kwargs):
450
        """ Update the argument index `self._argidx` dictionary taking into account the replacement number of splits."""
451
        for arg_name, value in kwargs.items():
452
            n_splits = len(value.split(op.sep))
453
454
            if n_splits < 1:
455
                raise ValueError('Error reading your replacement value "{}" for '
456
                                 'crumb argument "{}".'.format(value, arg_name))
457
            elif n_splits == 1:
458
                continue
459
460
            # n_splits > 1, so I have to update the position of the argument children
461
            childs = self._arg_children(arg_name)
462
            for child_name in childs:
463
                self._argidx[child_name] = self._argidx[child_name] + n_splits - 1
464
465
    def set_args(self, **kwargs):
466
        """ Set the crumb arguments in path to the given values in kwargs and update
467
        self accordingly.
468
        Parameters
469
        ----------
470
        kwargs: strings
471
472
        Returns
473
        -------
474
        crumb: Crumb
475
        """
476
        self._check_args(kwargs.keys(), self_args=self.all_args())
477
478
        # ignore for now the arguments that are in argval.
479
        # TODO: never change `_path`, make the `path` property to build up the path on runtime checking argval.
480
        for k in list(kwargs.keys()):
481
            if k in self._argval:
482
                kwargs.pop(k)
483
484
        self._path = self._replace(self._path, **kwargs)
485
        self._check()
486
487
        self._update_argidx(**kwargs)
488
        _dict_popitems(self.patterns, **kwargs)
489
        self._argval.update(**kwargs)
490
        return self
491
492
    def replace(self, **kwargs):
493
        """ Return a copy of self with the crumb arguments in
494
        `kwargs` replaced by its values.
495
        As an analogy to the `str.format` function this function could be called `format`.
496
        Parameters
497
        ----------
498
        kwargs: strings
499
500
        Returns
501
        -------
502
        crumb:
503
        """
504
        cr = self.copy(self)
505
        return cr.set_args(**kwargs)
506
507
    def _arg_parents(self, arg_name):
508
        """ Return a subdict with the open arguments name and index in `self._argidx`
509
        that come before `arg_name` in the crumb path. Include `arg_name` himself.
510
        Parameters
511
        ----------
512
        arg_name: str
513
514
        Returns
515
        -------
516
        arg_deps: Mapping[str, int]
517
        """
518
        argidx = self._find_arg(arg_name)
519
        return OrderedDict([(arg, idx) for arg, idx in self._open_arg_items() if idx <= argidx])
520
521
    def _arg_children(self, arg_name):
522
        """ Return a subdict with the open arguments name and index in `self._argidx`
523
        that come AFTER `arg_name` in the crumb path.
524
        Parameters
525
        ----------
526
        arg_name: str
527
528
        Returns
529
        -------
530
        arg_deps: Mapping[str, int]
531
        """
532
        argidx = self._find_arg(arg_name)
533
        return OrderedDict([(arg, idx) for arg, idx in self._open_arg_items() if idx > argidx])
534
535
    def _args_open_parents(self, arg_names):
536
        """ Return the name of the arguments that are dependencies of `arg_names`.
537
        Parameters
538
        ----------
539
        arg_names: Sequence[str]
540
541
        Returns
542
        -------
543
        rem_deps: Sequence[str]
544
        """
545
        started = False
546
        arg_dads = []
547
        for an in reversed(list(self.open_args())):  # take into account that argidx is ordered
548
            if an in arg_names:
549
                started = True
550
            else:
551
                if started:
552
                    arg_dads.append(an)
553
554
        return list(reversed(arg_dads))
555
556
    def values_map(self, arg_name='', check_exists=False):
557
        """ Return a list of tuples of crumb arguments with their values from the first argument
558
        until `arg_name`.
559
        Parameters
560
        ----------
561
        arg_name: str
562
            If empty will pick the arg_name of the last open argument of the Crumb.
563
564
        check_exists: bool
565
566
        Returns
567
        -------
568
        values_map: list of lists of 2-tuples
569
            I call values_map what is called `record` in pandas. It is a list of lists of 2-tuples, where each 2-tuple
570
            has the shape (arg_name, arg_value).
571
        """
572
        if not arg_name:
573
            arg_name, _ = self._last_open_arg()
574
575
        arg_deps = self._arg_parents(arg_name)
576
        values_map = None
577
        for arg in arg_deps:
578
            values_map = self._arg_values(arg, values_map)
579
580
        if check_exists:
581
            paths = [cr for cr in self.build_paths(values_map, make_crumbs=True)]
582
            values_map_checked = [args for args, path in zip(values_map, paths) if path.exists()]
583
        else:
584
            values_map_checked = values_map
585
586
        return sorted(values_map_checked)
587
588
    def build_paths(self, values_map, make_crumbs=True):
589
        """ Return a list of paths from each tuple of args from `values_map`
590
        Parameters
591
        ----------
592
        values_map: list of sequences of 2-tuple
593
            Example: [[('subject_id', 'haensel'), ('candy', 'lollipop.png')],
594
                      [('subject_id', 'gretel'),  ('candy', 'jujube.png')],
595
                     ]
596
597
        make_crumbs: bool
598
            If `make_crumbs` is True will create a Crumb for
599
            each element of the result.
600
            Default: True.
601
602
        Returns
603
        -------
604
        paths: list of str or list of Crumb
605
        """
606
        if make_crumbs:
607
            return [self.replace(**dict(val)) for val in values_map]
608
        else:
609
            return [self._replace(self._path, **dict(val)) for val in values_map]
610
611
    def ls(self, arg_name='', fullpath=True, make_crumbs=True, check_exists=False):
612
        """ Return the list of values for the argument crumb `arg_name`.
613
        This will also unfold any other argument crumb that appears before in the
614
        path.
615
        Parameters
616
        ----------
617
        arg_name: str
618
            Name of the argument crumb to be unfolded.
619
            If empty will pick the arg_name of the last open argument of the Crumb.
620
621
        fullpath: bool
622
            If True will build the full path of the crumb path, will also append
623
            the rest of crumbs not unfolded.
624
            If False will only return the values for the argument with name
625
            `arg_name`.
626
627
        make_crumbs: bool
628
            If `fullpath` and `make_crumbs` is True will create a Crumb for
629
            each element of the result.
630
631
        check_exists: bool
632
            If True will return only str, Crumb or Path if it exists
633
            in the file path, otherwise it may create file paths
634
            that don't have to exist.
635
636
        Returns
637
        -------
638
        values: list of Crumb or str
639
640
        Examples
641
        --------
642
        >>> cr = Crumb(op.join(op.expanduser('~'), '{user_folder}'))
643
        >>> user_folders = cr.ls('user_folder',fullpath=True,make_crumbs=True)
644
        """
645
        if not arg_name:
646
            arg_name, _ = self._last_open_arg()
647
648
        self._check_open_args([arg_name])
649
650
        start_sym, _ = self._start_end_syms
651
652
        # if the first chunk of the path is a parameter, I am not interested in this (for now)
653
        if self._path.startswith(start_sym):
654
            raise NotImplementedError("Cannot list paths that start with an argument. "
655
                                      "If this is a relative path, use the `abspath()` member function.")
656
657
        if make_crumbs and not fullpath:
658
            raise ValueError("`make_crumbs` can only work if `fullpath` is also True.")
659
660
        values_map = self.values_map(arg_name, check_exists=check_exists)
661
662
        if fullpath:
663
            paths = self.build_paths(values_map, make_crumbs=make_crumbs)
664
665
        else:
666
            paths = [dict(val)[arg_name] for val in values_map]
667
668
        return sorted(paths)
669
670
    def touch(self, exist_ok=True):
671
        """ Create a leaf directory and all intermediate ones using the non
672
        crumbed part of `crumb_path`.
673
        If the target directory already exists, raise an IOError if exist_ok
674
        is False. Otherwise no exception is raised.
675
        Parameters
676
        ----------
677
        crumb_path: str
678
679
        exist_ok: bool
680
            Default = True
681
682
        Returns
683
        -------
684
        nupath: str
685
            The new path created.
686
        """
687
        return self._touch(self.path, exist_ok=True)
688
689
    def joinpath(self, suffix):
690
        """ Return a copy of the current crumb with the `suffix` path appended.
691
        If suffix has crumb arguments, the whole crumb will be updated.
692
        Parameters
693
        ----------
694
        suffix: str
695
696
        Returns
697
        -------
698
        cr: Crumb
699
        """
700
        return Crumb(op.join(self._path, suffix))
701
702
    def exists(self):
703
        """ Return True if the current crumb path is a possibly existing path,
704
        False otherwise.
705
        Returns
706
        -------
707
        exists: bool
708
        """
709
        if not self.has_crumbs(self._path):
710
            return op.exists(str(self)) or op.islink(str(self))
711
712
        if not op.exists(self.split()[0]):
713
            return False
714
715
        last, _ = self._last_open_arg()
716
717
        paths = self.ls(last, fullpath=True, make_crumbs=False, check_exists=False)
718
719
        return any([self._split_exists(lp) for lp in paths])
720
721
    def has_files(self):
722
        """ Return True if the current crumb path has any file in its
723
        possible paths.
724
        Returns
725
        -------
726
        has_files: bool
727
        """
728
        if not op.exists(list(self.split())[0]):
729
            return False
730
731
        last, _ = self._last_open_arg()
732
        paths = self.ls(last, fullpath=True, make_crumbs=True, check_exists=True)
733
734
        return any([op.isfile(str(lp)) for lp in paths])
735
736
    def unfold(self):
737
        """ Return a list of all the existing paths until the last crumb argument.
738
        If there are no remaining open arguments,
739
        Returns
740
        -------
741
        paths: list of pathlib.Path
742
        """
743
        if list(self.open_args()):
744
            return self.ls(self._last_open_arg()[0], fullpath=True, make_crumbs=True, check_exists=True)
745
        else:
746
            return [self]
747
748
    def __getitem__(self, arg_name):
749
        """ Return the existing values of the crumb argument `arg_name`
750
        without removing duplicates.
751
        Parameters
752
        ----------
753
        arg_name: str
754
755
        Returns
756
        -------
757
        values: list of str
758
        """
759
        if arg_name in self._argval:
760
            return [self._argval[arg_name]]
761
        else:
762
            return self.ls(arg_name, fullpath=False, make_crumbs=False, check_exists=True)
763
764
    def __setitem__(self, key, value):
765
        if key not in self._argidx:
766
            raise KeyError("Expected `arg_name` to be one of ({}),"
767
                           " got {}.".format(list(self.open_args()), key))
768
        _ = self.set_args(**{key: value})
769
770
    def __ge__(self, other):
771
        return self._path >= str(other)
772
773
    def __le__(self, other):
774
        return self._path <= str(other)
775
776
    def __gt__(self, other):
777
        return self._path > str(other)
778
779
    def __lt__(self, other):
780
        return self._path < str(other)
781
782
    def __hash__(self):
783
        return self._path.__hash__()
784
785
    def __contains__(self, arg_name):
786
        return arg_name in self.all_args()
787
788
    def __repr__(self):
789
        return '{}("{}")'.format(type(self).__name__, self._path)
790
791
    def __str__(self):
792
        return str(self._path)
793
794
    def __eq__(self, other):
795
        """ Return True if `self` and `other` are equal, False otherwise.
796
        Parameters
797
        ----------
798
        other: Crumb
799
800
        Returns
801
        -------
802
        is_equal: bool
803
        """
804
        if self._path != other._path:
805
            return False
806
807
        if self._argidx != other._argidx:
808
            return False
809
810
        if self._argval != other._argval:
811
            return False
812
813
        if self._ignore != other._ignore:
814
            return False
815
816
        return True
817