Completed
Push — master ( da0b24...226022 )
by Alexandre M.
01:04
created

Crumb._update()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

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