Completed
Push — master ( 8984be...f44556 )
by Alexandre M.
01:19
created

Crumb.values_map()   C

Complexity

Conditions 7

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

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