Completed
Push — master ( 6d9b57...dc85d9 )
by Alexandre M.
51s
created

hansel.Crumb._path_split()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 2
rs 10
1
# -*- coding: utf-8 -*-
2
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3
# vi: set ft=python sts=4 ts=4 sw=4 et:
4
"""
5
Crumb class: the smart path model class.
6
"""
7
8
import os
9
import os.path     as op
10
from   copy        import deepcopy
11
from   collections import OrderedDict, Mapping, Sequence
12
from   pathlib     import Path
13
14
from   six import string_types
15
16
from   hansel.utils import remove_duplicates, list_children
17
18
19
class Crumb(object):
20
    """ The crumb path model class.
21
    Parameters
22
    ----------
23
    crumb_path: str
24
        A file or folder path with crumb arguments. See Examples.
25
26
    Examples
27
    --------
28
    >>> crumb = Crumb("{base_dir}/raw/{subject_id}/{session_id}/{modality}/{image}")
29
    >>> cr = Crumb(op.join(op.expanduser('~'), '{user_folder}'))
30
    """
31
    _arg_start_sym = '{'
32
    _arg_end_sym   = '}'
33
34
    def __init__(self, crumb_path):
35
        self._path   = self._get_path(crumb_path)
36
        self._argidx = OrderedDict()
37
        self._update()
38
39
    @property
40
    def path(self):
41
        """Return the current crumb path string."""
42
        return self._path
43
44
    @path.setter
45
    def path(self, value):
46
        """ Set the current crumb path string and updates the internal members.
47
        Parameters
48
        ----------
49
        value: str
50
            A file or folder path with crumb arguments. See Examples in class docstring.
51
        """
52
        self._path = value
53
        self._update()
54
55
    def _check(self):
56
        if not self.is_valid(self._path):
57
            raise ValueError("The current crumb path has errors, got {}.".format(self.path))
58
59
    def _update(self):
60
        """ Clean up, parse the current crumb path and fill the internal
61
        members for functioning."""
62
        self._clean()
63
        self._check()
64
        self._set_argidx()
65
        self._set_replace_func()
66
67
    def _clean(self):
68
        """ Clean up the private utility members, i.e., _argidx. """
69
        self._argidx = OrderedDict()
70
71
    @classmethod
72
    def _arg_name(cls, arg):
73
        """ Return the name of the argument given its crumb representation.
74
        Parameters
75
        ----------
76
        arg_crumb: str
77
78
        Returns
79
        -------
80
        arg_name: str
81
        """
82
        if not cls._is_crumb_arg(arg):
83
            raise ValueError("Expected an well formed crumb argument, "
84
                             "got {}.".format(arg))
85
        return arg[1:-1]
86
87
    def _arg_format(self, arg_name):
88
        """ Return the argument for its string `format()` representation.
89
        Parameters
90
        ----------
91
        arg_name: str
92
93
        Returns
94
        -------
95
        arg_format: str
96
        """
97
        return '{' + arg_name + '}'
98
99
    @classmethod
100
    def copy(cls, crumb):
101
        """ Return a deep copy of the given `crumb`.
102
        Parameters
103
        ----------
104
        crumb: str or Crumb
105
106
        Returns
107
        -------
108
        copy: Crumb
109
        """
110
        if isinstance(crumb, cls):
111
            return cls(crumb._path)
112
        elif isinstance(crumb, string_types):
113
            return cls.from_path(crumb)
114
        else:
115
            raise TypeError("Expected a Crumb or a str to copy, got {}.".format(type(crumb)))
116
117
    def _set_argidx(self):
118
        """ Initialize the self._argidx dict. It holds arg_name -> index.
119
        The index is the position in the whole `_path.split(op.sep)` where each argument is.
120
        """
121
        fs = self._path_split()
122
        for idx, f in enumerate(fs):
123
            if self._is_crumb_arg(f):
124
                self._argidx[self._arg_name(f)] = idx
125
126
    def _set_replace_func(self):
127
        """ Set the fastest replace algorithm depending on how
128
        many arguments the path has."""
129
        self._replace = self._replace2
130
        if len(self._argidx) > 5:
131
            self._replace = self._replace1
132
133
    def _find_arg(self, arg_name):
134
        """ Return the index in the current path of the crumb
135
        argument with name `arg_name`.
136
        """
137
        return self._argidx.get(arg_name, -1)
138
139
    def isabs(self):
140
        """ Return True if the current crumb path has an
141
        absolute path, False otherwise.
142
        This means that if it is valid and does not start with a `op.sep` character
143
        or hard disk letter.
144
        """
145
        if not self.is_valid(self._path):
146
            raise ValueError("The given crumb path has errors, got {}.".format(self.path))
147
148
        subp = self._path.split(self._arg_start_sym)[0]
149
        return op.isabs(subp)
150
151
    def abspath(self, first_is_basedir=False):
152
        """ Return a copy of `self` with an absolute crumb path.
153
        Add as prefix the absolute path to the current directory if the current
154
        crumb is not absolute.
155
        Parameters
156
        ----------
157
        first_is_basedir: bool
158
            If True and the current crumb path starts with a crumb argument and first_is_basedir,
159
            the first argument will be replaced by the absolute path to the current dir,
160
            otherwise the absolute path to the current dir will be added as a prefix.
161
162
163
        Returns
164
        -------
165
        abs_crumb: Crumb
166
        """
167
        if not self.is_valid(self._path):
168
            raise ValueError("The given crumb path has errors, got {}.".format(self.path))
169
170
        if self.isabs():
171
            return deepcopy(self)
172
173
        return self.copy(self._abspath(first_is_basedir=first_is_basedir))
174
175
    def _path_split(self):
176
        return self._path.split(op.sep)
177
178
    def _abspath(self, first_is_basedir=False):
179
        """ Return the absolute path of the current crumb path.
180
        Parameters
181
        ----------
182
        first_is_basedir: bool
183
            If True and the current crumb path starts with a crumb argument and first_is_basedir,
184
            the first argument will be replaced by the absolute path to the current dir,
185
            otherwise the absolute path to the current dir will be added as a prefix.
186
187
188
        Returns
189
        -------
190
        abspath: str
191
        """
192
        if not self.has_crumbs(self._path):
193
             return op.abspath(self._path)
194
195
        splt = self._path_split()
196
        path = []
197
        if self._is_crumb_arg(splt[0]):
198
            path.append(op.abspath(op.curdir))
199
200
        if not first_is_basedir:
201
            path.append(splt[0])
202
203
        if splt[1:]:
204
            path.extend(splt[1:])
205
206
        return op.sep.join(path)
207
208
    def split(self):
209
        """ Return a list of sub-strings of the current crumb path where the
210
            path parts are separated from the crumb arguments.
211
212
        Returns
213
        -------
214
        crumbs: list of str
215
        """
216
        return self._split(self._path)
217
218
    def _default_map(self):
219
        """ Return the dict with the default format values of the
220
            crumb arguments."""
221
        return {v: self._arg_format(v) for v in self._argidx}
222
223
    @classmethod
224
    def _split(cls, crumb_path):
225
        """ Return a list of sub-strings of `crumb_path` where the
226
            path parts are separated from the crumb arguments.
227
        """
228
        crumb_path = cls._get_path(crumb_path)
229
230
        splt = []
231
        tmp = '/' if crumb_path.startswith(op.sep) else ''
232
        for i in crumb_path.split(op.sep):
233
            if i.startswith(cls._arg_start_sym):
234
                splt.append(tmp)
235
                tmp = ''
236
                splt.append(i)
237
            else:
238
                tmp = op.join(tmp, i)
239
240
        return splt
241
242
    @classmethod
243
    def is_valid(cls, crumb_path):
244
        """ Return True if `crumb_path` is a well formed path with crumb arguments,
245
        False otherwise.
246
        Parameters
247
        ----------
248
        crumb_path: str
249
250
        Returns
251
        -------
252
        is_valid: bool
253
        """
254
        crumb_path = cls._get_path(crumb_path)
255
256
        splt = crumb_path.split(op.sep)
257
        for crumb in splt:
258
            if op.isdir(crumb):
259
                continue
260
261
            if cls._is_crumb_arg(crumb):
262
                crumb = cls._arg_name(crumb)
263
264
            if cls._arg_start_sym in crumb or cls._arg_end_sym in crumb:
265
                return False
266
267
        return True
268
269
    @classmethod
270
    def _is_crumb_arg(cls, crumb_arg):
271
        """ Returns True if `crumb_arg` is a well formed
272
        crumb argument.
273
        Parameters
274
        ----------
275
        crumb_arg: str
276
            The string representing a crumb argument, e.g., "{sample_id}"
277
278
        Returns
279
        -------
280
        is_crumb_arg: bool
281
        """
282
        if not isinstance(crumb_arg, string_types):
283
            return False
284
285
        return crumb_arg.startswith(cls._arg_start_sym) and crumb_arg.endswith(cls._arg_end_sym)
286
287
    @classmethod
288
    def has_crumbs(cls, crumb_path):
289
        """ Return True if the `crumb_path.split(op.sep)` has item which is a crumb argument
290
        that starts with '{' and ends with '}'."""
291
        crumb_path = cls._get_path(crumb_path)
292
293
        splt = crumb_path.split(op.sep)
294
        for i in splt:
295
            if cls._is_crumb_arg(i):
296
                return True
297
298
        return False
299
300
    @classmethod
301
    def _get_path(cls, crumb_path):
302
        """ Return the path string from `crumb_path`.
303
        Parameters
304
        ----------
305
        crumb_path: str or Crumb
306
307
        Returns
308
        -------
309
        path: str
310
        """
311
        if isinstance(crumb_path, cls):
312
            crumb_path = crumb_path._path
313
314
        if not isinstance(crumb_path, string_types):
315
            raise TypeError("Expected `crumb_path` to be a {}, got {}.".format(string_types, type(crumb_path)))
316
317
        return crumb_path
318
319
    @classmethod
320
    def from_path(cls, crumb_path):
321
        """ Create an instance of Crumb or pathlib.Path out of `crumb_path`.
322
        It will return a Crumb if `crumb_path` has crumbs or
323
        Parameters
324
        ----------
325
        val: str, Crumb or pathlib.Path
326
327
        Returns
328
        -------
329
        path: Crumb or pathlib.Path
330
        """
331
        if isinstance(crumb_path, (cls, Path)):
332
            return crumb_path
333
334
        if isinstance(crumb_path, string_types):
335
            if cls.has_crumbs(crumb_path):
336
                return cls(crumb_path)
337
            else:
338
                return Path(crumb_path)
339
        else:
340
            raise TypeError("Expected a `val` to be a `str`, got {}.".format(type(crumb_path)))
341
342
    def _replace1(self, **kwargs):
343
        if not kwargs:
344
            return self._path
345
346
        args = self._default_map()
347
        for k in kwargs:
348
            if k not in args:
349
                raise KeyError("Could not find argument {}"
350
                               " in `path` {}.".format(k, self._path))
351
352
            args[k] = kwargs[k]
353
354
        return self._path.format_map(args)
355
356
    def _replace2(self, **kwargs):
357
        if not kwargs:
358
            return self._path
359
360
        path = self._path
361
        for k in kwargs:
362
            karg = self._arg_format(k)
363
            if k not in path:
364
                raise KeyError("Could not find argument {} in"
365
                               " `path` {}.".format(k, self._path))
366
367
            path = path.replace(karg, kwargs[k])
368
369
        return path
370
371
    def _lastarg(self):
372
        """ Return the name and idx of the last argument."""
373
        for arg, idx in reversed(list(self._argidx.items())):
374
            return arg, idx
375
376
    def _firstarg(self):
377
        """ Return the name and idx of the first argument."""
378
        for arg, idx in self._argidx.items():
379
            return arg, idx
380
381
    def _is_firstarg(self, arg_name):
382
        """ Return True if `arg_name` is the first argument."""
383
        # Take into account that self._argidx is OrderedDict
384
        return arg_name == self._firstarg()[0]
385
386
    def _arg_values(self, arg_name, arg_values=None):
387
        """ Return the existing values in the file system for the crumb argument
388
        with name `arg_name`.
389
        The `arg_values` must be a sequence with the tuples with valid values of the dependent
390
        (previous in the path) crumb arguments.
391
        The format of `arg_values` work in such a way that `self._path.format(dict(arg_values[0]))`
392
        would give me a valid path or crumb.
393
        Parameters
394
        ----------
395
        arg_name: str
396
397
        arg_values: list of tuples
398
399
        Returns
400
        -------
401
        vals: list of tuples
402
403
        Raises
404
        ------
405
        ValueError: if `arg_values` is None and `arg_name` is not the
406
        first crumb argument in self._path
407
408
        IOError: if this crosses to any path that is non-existing.
409
        """
410
        if arg_values is None and not self._is_firstarg(arg_name):
411
            raise ValueError("Cannot get the list of values for {} if"
412
                             " the previous arguments are not filled"
413
                             " in `paths`.".format(arg_name))
414
415
        aidx = self._find_arg(arg_name)
416
417
        # check if the path is absolute, do it absolute
418
        apath = self._abspath()
419
        splt = apath.split(op.sep)
420
421
        if aidx == len(splt) - 1:  # this means we have to list files too
422
            just_dirs = False
423
        else:  # this means we have to list folders
424
            just_dirs = True
425
426
        vals = []
427
        if arg_values is None:
428
            base = op.sep.join(splt[:aidx])
429
            vals = [[(arg_name, val)] for val in list_children(base, just_dirs=just_dirs)]
430
        else:
431
            for aval in arg_values:
432
                #  create the part of the crumb path that is already specified
433
                path = self._split(self._replace(**dict(aval)))[0]
434
435
                #  list the children of `path`
436
                subpaths = list_children(path, just_dirs=just_dirs)
437
438
                #  extend `val` tuples with the new list of values for `aval`
439
                vals.extend([aval + [(arg_name, sp)] for sp in subpaths])
440
441
        return vals
442
443
    def replace(self, **kwargs):
444
        """ Return a copy of self with the crumb arguments in
445
        `kwargs` replaced by its values.
446
        Parameters
447
        ----------
448
        kwargs: strings
449
450
        Returns
451
        -------
452
        crumb:
453
        """
454
        for arg_name in kwargs:
455
            if arg_name not in self._argidx:
456
                raise KeyError("Expected `arg_name` to be one of ({}),"
457
                                 " got {}.".format(list(self._argidx), arg_name))
458
459
        cr = self.copy(self)
460
        cr._path = cr._replace(**kwargs)
461
        return Crumb.from_path(cr._path)
462
463
    def _arg_deps(self, arg_name):
464
        """ Return a subdict of `self._argidx` with the
465
         values from the crumb arguments that come before
466
         `arg_name` in the crumb path.
467
        Parameters
468
        ----------
469
        arg_name: str
470
471
        Returns
472
        -------
473
        arg_deps: Mapping[str, int]
474
        """
475
        argidx = self._find_arg(arg_name)
476
        return OrderedDict([(arg, idx) for arg, idx in self._argidx.items() if idx <= argidx])
477
478
    def values_map(self, arg_name, check_exists=False):
479
        """
480
481
        Parameters
482
        ----------
483
        arg_name: str
484
485
        check_exists: bool
486
487
        Returns
488
        -------
489
        values_map
490
        """
491
        arg_deps = self._arg_deps(arg_name)
492
        values_map = None
493
        for arg in arg_deps:
494
            values_map = self._arg_values(arg, values_map)
495
496
        if check_exists:
497
            paths = [self.from_path(path) for path in self._build_paths(values_map)]
498
            values_map_checked = [args for args, path in zip(values_map, paths) if path.exists()]
499
        else:
500
            values_map_checked = values_map
501
502
        return values_map_checked
503
504
    def _build_paths(self, values_map):
505
        """ Return a list of paths from each tuple of args from `values_map`
506
        Parameters
507
        ----------
508
        values_map: list of sequences of 2-tuple
509
510
        Returns
511
        -------
512
        paths: list of str
513
        """
514
        return [self._replace(**dict(val)) for val in values_map]
515
516
    def ls(self, arg_name, fullpath=True, rm_dups=False, make_crumbs=True, check_exists=False):
517
        """
518
        Return the list of values for the argument crumb `arg_name`.
519
        This will also unfold any other argument crumb that appears before in the
520
        path.
521
        Parameters
522
        ----------
523
        arg_name: str
524
            Name of the argument crumb to be unfolded.
525
526
        fullpath: bool
527
            If True will build the full path of the crumb path, will also append
528
            the rest of crumbs not unfolded.
529
            If False will only return the values for the argument with name
530
            `arg_name`.
531
532
        rm_dups: bool
533
            If True will remove and sort the duplicate values from the result.
534
            Otherwise it will leave it as it is.
535
536
        make_crumbs: bool
537
            If `fullpath` and `make_crumbs` is True will create a Crumb or a pathlib.Path
538
            for each element of the result. This will depende if the result item still has
539
            crumb arguments or not.
540
541
        check_exists: bool
542
            If True will return only str, Crumb or Path if it exists
543
            in the file path, otherwise it may create file paths
544
            that don't have to exist.
545
546
        Returns
547
        -------
548
        values: list of str or Crumb
549
550
        Examples
551
        --------
552
        >>> cr = Crumb(op.join(op.expanduser('~'), '{user_folder}'))
553
        >>> user_folders = cr.ls('user_folder', fullpath=True, rm_dups=True, make_crumbs=True)
554
        """
555
        if arg_name not in self._argidx:
556
            raise ValueError("Expected `arg_name` to be one of ({}),"
557
                             " got {}.".format(list(self._argidx), arg_name))
558
559
        # if the first chunk of the path is a parameter, I am not interested in this (for now)
560
        if self._path.startswith(self._arg_start_sym):
561
            raise NotImplementedError("Can't list paths that starts"
562
                                      " with an argument.")
563
564
        if make_crumbs and not fullpath:
565
            raise ValueError("`make_crumbs` can only work if `fullpath` is also True.")
566
567
        values_map = self.values_map(arg_name, check_exists=check_exists)
568
569
        if not fullpath and not make_crumbs:
570
            paths = [dict(val)[arg_name] for val in values_map]
571
            if rm_dups:
572
                paths = remove_duplicates(paths)
573
574
        else:
575
            paths = self._build_paths(values_map)
576
            if rm_dups:
577
                paths = remove_duplicates(paths)
578
579
            if fullpath and make_crumbs:
580
                paths = [self.from_path(path) for path in paths]
581
582
        return paths
583
584
        # if check_exists:
585
        #     return self._ls_check_exists(arg_name, values_map=values_map,
586
        #                                  fullpath=fullpath,
587
        #                                  rm_dups=rm_dups,
588
        #                                  make_crumbs=make_crumbs)
589
        # else:
590
        #     return self._ls_no_check_exists(arg_name, values_map=values_map,
591
        #                                     fullpath=fullpath,
592
        #                                     rm_dups=rm_dups,
593
        #                                     make_crumbs=make_crumbs)
594
    #
595
    # def _ls_no_check_exists(self, arg_name, values_map, fullpath, rm_dups, make_crumbs):
596
    #     if not fullpath:  # this means we can return the list of crumbs directly
597
    #         values = [dict(val)[arg_name] for val in values_map]
598
    #     else:  # this means we have to build the full paths
599
    #         values = [self._replace(**dict(val)) for val in values_map]
600
    #
601
    #     if not rm_dups:
602
    #         values = remove_duplicates(values)
603
    #
604
    #     if fullpath and make_crumbs:
605
    #         values = [self.from_path(val) for val in values]
606
    #
607
    #     return values
608
    #
609
    # def _ls_check_exists(self, arg_name, values_map, fullpath, rm_dups, make_crumbs):
610
    #
611
    #     paths = [self._replace(**dict(val)) for val in values_map]
612
    #     paths = [self.from_path(val) for val in paths]
613
    #     paths = [val for val in paths if val.exists()]
614
    #
615
    #     if not fullpath:
616
    #         argidx = self._argidx[arg_name]
617
    #         values = [str(val).split(op.sep)[argidx] for val in paths]
618
    #     else:
619
    #         if make_crumbs:
620
    #             values = paths
621
    #         else:
622
    #             values = [str(val) for val in paths]
623
    #
624
    #     return values
625
626
    def _remaining_deps(self, arg_names):
627
        """ Return the name of the arguments that are dependencies of `arg_names`.
628
        Parameters
629
        ----------
630
        arg_names: Sequence[str]
631
632
        Returns
633
        -------
634
        rem_deps: Sequence[str]
635
        """
636
        started = False
637
        rem_deps = []
638
        for an in reversed(list(self._argidx.keys())):  # take into account that argidx is ordered
639
            if an in arg_names:
640
                started = True
641
            else:
642
                if started:
643
                    rem_deps.append(an)
644
645
        return rem_deps
646
647
    def touch(self):
648
        """ Create a leaf directory and all intermediate ones
649
        using the non crumbed part of `crumb_path`.
650
        If the target directory already exists, raise an IOError
651
        if exist_ok is False. Otherwise no exception is raised.
652
        Parameters
653
        ----------
654
        crumb_path: str
655
656
        exist_ok: bool
657
            Default = True
658
659
        Returns
660
        -------
661
        nupath: str
662
            The new path created.
663
        """
664
        return self._touch(self._path)
665
666
    def joinpath(self, suffix):
667
        """ Return a copy of the current crumb with the `suffix` path appended.
668
        If suffix has crumb arguments, the whole crumb will be updated.
669
        Parameters
670
        ----------
671
        suffix: str
672
673
        Returns
674
        -------
675
        cr: Crumb
676
        """
677
        return Crumb(op.join(self._path, suffix))
678
679
    @classmethod
680
    def _touch(cls, crumb_path, exist_ok=True):
681
        """ Create a leaf directory and all intermediate ones
682
        using the non crumbed part of `crumb_path`.
683
        If the target directory already exists, raise an IOError
684
        if exist_ok is False. Otherwise no exception is raised.
685
        Parameters
686
        ----------
687
        crumb_path: str
688
689
        exist_ok: bool
690
            Default = True
691
692
        Returns
693
        -------
694
        nupath: str
695
            The new path created.
696
        """
697
        if cls.has_crumbs(crumb_path):
698
            nupath = cls._split(crumb_path)[0]
699
        else:
700
            nupath = crumb_path
701
702
        if op.exists(nupath) and not exist_ok:
703
            raise IOError("Folder {} already exists.".format(nupath))
704
705
        try:
706
            os.makedirs(nupath, exist_ok=exist_ok)
707
        except:
708
            raise
709
        else:
710
            return nupath
711
712
    def exists(self):
713
        """ Return True if the current crumb path is a possibly existing path,
714
        False otherwise.
715
        Returns
716
        -------
717
        exists: bool
718
        """
719
        if not op.exists(self.split()[0]):
720
            return False
721
722
        last, _ = self._lastarg()
723
        paths = self.ls(last,
724
                        fullpath     = True,
725
                        make_crumbs  = False,
726
                        rm_dups   = True,
727
                        check_exists = False)
728
729
        return all([self._split_exists(lp) for lp in paths])
730
731
    def has_files(self):
732
        """ Return True if the current crumb path has any file in its
733
        possible paths.
734
        Returns
735
        -------
736
        has_files: bool
737
        """
738
        if not op.exists(self.split()[0]):
739
            return False
740
741
        last, _ = self._lastarg()
742
        paths = self.ls(last,
743
                        fullpath     = True,
744
                        make_crumbs  = True,
745
                        rm_dups      = False,
746
                        check_exists = True)
747
748
        return any([op.isfile(str(lp)) for lp in paths])
749
750
    def unfold(self):
751
        """ Return a list of all the existing paths until the last crumb argument.
752
        Returns
753
        -------
754
        paths: list of pathlib.Path
755
        """
756
        return self.ls(self._lastarg()[0],
757
                       fullpath    = True,
758
                       rm_dups     = True,
759
                       make_crumbs = True,
760
                       check_exists= True)
761
762
    @classmethod
763
    def _split_exists(cls, crumb_path):
764
        """ Return True if the part without crumb arguments of `crumb_path`
765
        is an existing path or a symlink, False otherwise.
766
        Returns
767
        -------
768
        exists: bool
769
        """
770
        if cls.has_crumbs(crumb_path):
771
            rpath = cls._split(crumb_path)[0]
772
        else:
773
            rpath = str(crumb_path)
774
775
        return op.exists(rpath) or op.islink(rpath)
776
777
    def __getitem__(self, arg_name):
778
        """ Return the existing values of the crumb argument `arg_name`
779
        without removing duplicates.
780
        Parameters
781
        ----------
782
        arg_name: str
783
784
        Returns
785
        -------
786
        values: list of str
787
        """
788
        return self.ls(arg_name,
789
                       fullpath    = False,
790
                       rm_dups     = False,
791
                       make_crumbs = False,
792
                       check_exists= True)
793
794
    def __setitem__(self, key, value):
795
        if key not in self._argidx:
796
            raise KeyError("Expected `arg_name` to be one of ({}),"
797
                           " got {}.".format(list(self._argidx), key))
798
799
        self._path = self._replace(**{key: value})
800
        self._update()
801
802
    def __ge__(self, other):
803
        return self._path >= str(other)
804
805
    def __le__(self, other):
806
        return self._path <= str(other)
807
808
    def __gt__(self, other):
809
        return self._path > str(other)
810
811
    def __lt__(self, other):
812
        return self._path < str(other)
813
814
    def __hash__(self):
815
        return self._path.__hash__()
816
817
    def __contains__(self, item):
818
        return item in self._argidx
819
820
    def __repr__(self):
821
        return '{}("{}")'.format(__class__.__name__, self._path)
822
823
    def __str__(self):
824
        return str(self._path)
825
826
    def __eq__(self, other):
827
        """ Return True if `self` and `other` are equal, False otherwise.
828
        Parameters
829
        ----------
830
        other: Crumb
831
832
        Returns
833
        -------
834
        is_equal: bool
835
        """
836
        if self._path != other._path:
837
            return False
838
839
        if self._argidx != other._argidx:
840
            return False
841
842
        return True
843
844