Completed
Push — master ( 6d4690...d70cd6 )
by Alexandre M.
01:26
created

Crumb.ls()   D

Complexity

Conditions 9

Size

Total Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 12
Bugs 3 Features 0
Metric Value
cc 9
c 12
b 3
f 0
dl 0
loc 68
rs 4.3814

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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