Completed
Push — master ( 88e002...ac3c9d )
by Alexandre M.
01:01
created

hansel.Crumb._extend_arg_values()   B

Complexity

Conditions 4

Size

Total Lines 22

Duplication

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