Completed
Push — master ( f0016f...4d76e9 )
by Alexandre M.
01:09
created

Crumb   F

Complexity

Total Complexity 133

Size/Duplication

Total Lines 795
Duplicated Lines 0 %

Importance

Changes 30
Bugs 9 Features 4
Metric Value
c 30
b 9
f 4
dl 0
loc 795
rs 1.263
wmc 133

55 Methods

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

How to fix   Complexity   

Complex Class

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