Completed
Push — master ( 97fd67...53d194 )
by Alexandre M.
01:02
created

hansel.Crumb._check_ls_params()   A

Complexity

Conditions 4

Size

Total Lines 10

Duplication

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