Completed
Push — master ( c8d778...5b8e36 )
by Alexandre M.
01:11
created

Crumb.set_patterns()   A

Complexity

Conditions 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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