Completed
Push — master ( 1af1a8...5598ba )
by Alexandre M.
01:00
created

hansel.Crumb   F

Complexity

Total Complexity 125

Size/Duplication

Total Lines 728
Duplicated Lines 0 %
Metric Value
dl 0
loc 728
rs 1.263
wmc 125

54 Methods

Rating   Name   Duplication   Size   Complexity  
A patterns() 0 4 2
A clear_pattern() 0 3 1
A _set_match_function() 0 13 4
A set_pattern() 0 3 1
A is_valid() 0 8 2
A __init__() 0 11 2
A _update() 0 4 1
B values_map() 0 28 5
A path() 0 4 1
A __contains__() 0 2 1
A __hash__() 0 2 1
A __ge__() 0 2 1
A _first_open_arg() 0 4 2
A __eq__() 0 20 4
A abspath() 0 21 2
B _abspath() 0 25 4
A _arg_values_from_base() 0 10 2
A _arg_parents() 0 13 3
A arg_values() 0 4 1
A all_args() 0 9 1
A __lt__() 0 2 1
A has_crumbs() 0 7 2
A replace() 0 14 1
B copy() 0 23 4
A __setitem__() 0 2 1
A open_args() 0 5 2
A __le__() 0 2 1
A isabs() 0 7 1
B _extend_arg_values() 0 22 4
A has_files() 0 14 3
A has_set() 0 4 1
A _args_open_parents() 0 20 4
B _check_args() 0 15 6
B _arg_values() 0 58 6
A joinpath() 0 12 1
A _check_ls_params() 0 10 4
A exists() 0 17 4
A unfold() 0 11 2
A split() 0 9 1
A __gt__() 0 2 1
A _check_open_args() 0 13 1
A _last_open_arg() 0 8 3
A from_path() 0 19 4
A clear() 0 3 1
A _is_first_open_arg() 0 4 1
A update() 0 23 3
B ls() 0 48 5
A __repr__() 0 2 1
A __getitem__() 0 15 2
A touch() 0 18 1
A _open_arg_items() 0 15 2
B build_paths() 0 22 4
A __str__() 0 2 1
A _build_and_check() 0 4 4

How to fix   Complexity   

Complex Class

Complex classes like hansel.Crumb often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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