41
41
42
42
# stdlib
43
43
import contextlib
44
+ import fnmatch
44
45
import gzip
45
46
import json
46
47
import os
47
48
import pathlib
48
49
import shutil
49
50
import stat
50
51
import sys
51
- from typing import IO , Any , Callable , Iterable , List , Optional , TypeVar , Union
52
+ from collections import deque
53
+ from typing import IO , Any , Callable , Iterable , Iterator , List , Optional , TypeVar , Union
52
54
53
55
# this package
54
56
from domdf_python_tools .typing import JsonLibrary , PathLike
69
71
"WindowsPathPlus" ,
70
72
"in_directory" ,
71
73
"_P" ,
74
+ "_PP" ,
72
75
"traverse_to_file" ,
76
+ "matchglob" ,
77
+ "unwanted_dirs" ,
73
78
]
74
79
75
80
newline_default = object ()
81
86
.. versionchanged:: 1.7.0 Now bound to :class:`pathlib.Path`.
82
87
"""
83
88
89
+ _PP = TypeVar ("_PP" , bound = "PathPlus" )
90
+ """
91
+ .. versionadded:: 2.3.0
92
+ """
93
+
94
+ unwanted_dirs = (".git" , "venv" , ".venv" , ".mypy_cache" , "__pycache__" , ".pytest_cache" , ".tox" , ".tox4" )
95
+ """
96
+ A list of directories which will likely be unwanted when searching directory trees for files.
97
+
98
+ .. versionadded:: 2.3.0
99
+ """
100
+
84
101
85
102
def append (var : str , filename : PathLike , ** kwargs ) -> int :
86
103
"""
@@ -167,7 +184,7 @@ def maybe_make(directory: PathLike, mode: int = 0o777, parents: bool = False):
167
184
:param mode: Combined with the process’ umask value to determine the file mode and access flags
168
185
:param parents: If :py:obj:`False` (the default), a missing parent raises a :class:`FileNotFoundError`.
169
186
If :py:obj:`True`, any missing parents of this path are created as needed; they are created with the
170
- default permissions without taking mode into account (mimicking the POSIX mkdir -p command).
187
+ default permissions without taking mode into account (mimicking the POSIX `` mkdir -p`` command).
171
188
:no-default parents:
172
189
173
190
.. versionchanged:: 1.6.0 Removed the ``'exist_ok'`` option, since it made no sense in this context.
@@ -308,17 +325,15 @@ class PathPlus(pathlib.Path):
308
325
"""
309
326
Subclass of :class:`pathlib.Path` with additional methods and a default encoding of UTF-8.
310
327
311
- Path represents a filesystem path but unlike PurePath, also offers
312
- methods to do system calls on path objects. Depending on your system,
313
- instantiating a Path will return either a PosixPath or a WindowsPath
314
- object. You can also instantiate a PosixPath or WindowsPath directly,
315
- but cannot instantiate a WindowsPath on a POSIX system or vice versa.
328
+ Path represents a filesystem path but unlike :class:`~.PurePath`, also offers
329
+ methods to do system calls on path objects.
330
+ Depending on your system, instantiating a :class:`~.PathPlus` will return
331
+ either a :class:`~.PosixPathPlus` or a :class:`~.WindowsPathPlus`. object.
332
+ You can also instantiate a :class:`PosixPath` or :class:`WindowsPath` directly,
333
+ but cannot instantiate a :class:`WindowsPath` on a POSIX system or vice versa.
316
334
317
335
.. versionadded:: 0.3.8
318
-
319
- .. versionchanged:: 0.5.1
320
-
321
- Defaults to Unix line endings (``LF``) on all platforms.
336
+ .. versionchanged:: 0.5.1 Defaults to Unix line endings (``LF``) on all platforms.
322
337
"""
323
338
324
339
__slots__ = ("_accessor" , )
@@ -380,7 +395,7 @@ def maybe_make(
380
395
381
396
.. versionchanged:: 1.6.0 Removed the ``'exist_ok'`` option, since it made no sense in this context.
382
397
383
- .. note ::
398
+ .. attention ::
384
399
385
400
This will fail silently if a file with the same name already exists.
386
401
This appears to be due to the behaviour of :func:`os.mkdir`.
@@ -562,9 +577,7 @@ def dump_json(
562
577
rather than :meth:`PathPlus.write_text <domdf_python_tools.paths.PathPlus.write_text>`,
563
578
and returns :py:obj:`None` rather than :class:`int`.
564
579
565
- .. versionchanged:: 1.9.0
566
-
567
- Added the ``compress`` keyword-only argument.
580
+ .. versionchanged:: 1.9.0 Added the ``compress`` keyword-only argument.
568
581
"""
569
582
570
583
if compress :
@@ -602,9 +615,7 @@ def load_json(
602
615
603
616
:return: The deserialised JSON data.
604
617
605
- .. versionchanged:: 1.9.0
606
-
607
- Added the ``compress`` keyword-only argument.
618
+ .. versionchanged:: 1.9.0 Added the ``compress`` keyword-only argument.
608
619
"""
609
620
610
621
if decompress :
@@ -676,12 +687,12 @@ def replace(self: _P, target: Union[str, pathlib.PurePath]) -> _P: # type: igno
676
687
677
688
Returns the new Path instance pointing to the target path.
678
689
690
+ .. versionadded:: 0.3.8 for Python 3.8 and above
691
+ .. versionadded:: 0.11.0 for Python 3.6 and Python 3.7
692
+
679
693
:param target:
680
694
681
695
:returns: The new Path instance pointing to the target path.
682
-
683
- .. versionadded:: 0.3.8 for Python 3.8 and above
684
- .. versionadded:: 0.11.0 for Python 3.6 and Python 3.7
685
696
"""
686
697
687
698
self ._accessor .replace (self , target ) # type: ignore
@@ -723,12 +734,10 @@ def is_relative_to(self, *other: Union[str, os.PathLike]) -> bool:
723
734
r"""
724
735
Returns whether the path is relative to another path.
725
736
726
- :param \*other:
727
-
728
- :rtype:
729
-
730
737
.. versionadded:: 0.3.8 for Python 3.9 and above
731
738
.. versionadded:: 1.4.0 for Python 3.6 and Python 3.7
739
+
740
+ :param \*other:
732
741
"""
733
742
734
743
try :
@@ -741,19 +750,54 @@ def abspath(self) -> "PathPlus":
741
750
"""
742
751
Return the absolute version of the path.
743
752
744
- :rtype:
745
-
746
753
.. versionadded:: 1.3.0
747
754
"""
748
755
749
756
return self .__class__ (os .path .abspath (self ))
750
757
758
+ def iterchildren (
759
+ self : _PP ,
760
+ exclude_dirs : Optional [Iterable [str ]] = unwanted_dirs ,
761
+ match : Optional [str ] = None ,
762
+ ) -> Iterator [_PP ]:
763
+ """
764
+ Returns an iterator over all children (files and directories) of the current path object.
765
+
766
+ .. versionadded:: 2.3.0
767
+
768
+ :param exclude_dirs: A list of directory names which should be excluded from the output,
769
+ together with their children.
770
+ :param match: A pattern to match filenames against.
771
+ The pattern should be in the format taken by :func:`~.matchglob`.
772
+ """
773
+
774
+ if not self .is_dir ():
775
+ return
776
+
777
+ if exclude_dirs is None :
778
+ exclude_dirs = ()
779
+
780
+ if match and not os .path .isabs (match ):
781
+ match = (self / match ).as_posix ()
782
+
783
+ file : _PP
784
+ for file in self .iterdir (): # type: ignore
785
+ parts = file .parts
786
+ if any (d in parts for d in exclude_dirs ):
787
+ continue
788
+
789
+ if file .is_dir ():
790
+ yield from file .iterchildren (exclude_dirs , match )
791
+
792
+ if match is None or (match is not None and matchglob (file , match )):
793
+ yield file
794
+
751
795
752
796
class PosixPathPlus (PathPlus , pathlib .PurePosixPath ):
753
797
"""
754
798
:class:`~.PathPlus` subclass for non-Windows systems.
755
799
756
- On a POSIX system, instantiating a PathPlus object should return an instance of this class.
800
+ On a POSIX system, instantiating a :class:`~. PathPlus` object should return an instance of this class.
757
801
758
802
.. versionadded:: 0.3.8
759
803
"""
@@ -765,7 +809,7 @@ class WindowsPathPlus(PathPlus, pathlib.PureWindowsPath):
765
809
"""
766
810
:class:`~.PathPlus` subclass for Windows systems.
767
811
768
- On a Windows system, instantiating a PathPlus object should return an instance of this class.
812
+ On a Windows system, instantiating a :class:`~. PathPlus` object should return an instance of this class.
769
813
770
814
.. versionadded:: 0.3.8
771
815
"""
@@ -798,11 +842,11 @@ def traverse_to_file(base_directory: _P, *filename: PathLike, height: int = -1)
798
842
r"""
799
843
Traverse the parents of the given directory until the desired file is found.
800
844
845
+ .. versionadded:: 1.7.0
846
+
801
847
:param base_directory: The directory to start searching from
802
848
:param \*filename: The filename(s) to search for
803
849
:param height: The maximum height to traverse to.
804
-
805
- .. versionadded:: 1.7.0
806
850
"""
807
851
808
852
if not filename :
@@ -817,3 +861,60 @@ def traverse_to_file(base_directory: _P, *filename: PathLike, height: int = -1)
817
861
return directory
818
862
819
863
raise FileNotFoundError (f"'{ filename [0 ]!s} ' not found in { base_directory } " )
864
+
865
+
866
+ def matchglob (filename : PathLike , pattern ):
867
+ """
868
+ Given a filename and a glob pattern, return whether the filename matches the glob.
869
+
870
+ .. versionadded:: 2.3.0
871
+
872
+ :param filename:
873
+ :param pattern: A pattern structured like a filesystem path, where each element consists of the glob syntax.
874
+ Each element is matched by :mod:`fnmatch`.
875
+ The special element ``**`` matches zero or more files or directories.
876
+
877
+ .. seealso::
878
+
879
+ :wikipedia:`Glob (programming)#Syntax` on Wikipedia
880
+ """
881
+
882
+ filename = PathPlus (filename )
883
+
884
+ pattern_parts = deque (pathlib .PurePath (pattern ).parts )
885
+ filename_parts = deque (filename .parts )
886
+
887
+ if not pattern_parts [- 1 ]:
888
+ pattern_parts .pop ()
889
+
890
+ while True :
891
+ if not pattern_parts and not filename_parts :
892
+ return True
893
+
894
+ pattern_part = pattern_parts .popleft ()
895
+
896
+ if pattern_part == "**" and not filename_parts :
897
+ return True
898
+ else :
899
+ filename_part = filename_parts .popleft ()
900
+
901
+ if pattern_part == "**" :
902
+ if not pattern_parts or not filename_parts :
903
+ return True
904
+
905
+ while pattern_part == "**" :
906
+ pattern_part = pattern_parts .popleft ()
907
+
908
+ if fnmatch .fnmatchcase (filename_part , pattern_part ):
909
+ continue
910
+ else :
911
+ while not fnmatch .fnmatchcase (filename_part , pattern_part ):
912
+ if not filename_parts :
913
+ return False
914
+
915
+ filename_part = filename_parts .popleft ()
916
+
917
+ elif fnmatch .fnmatchcase (filename_part , pattern_part ):
918
+ continue
919
+ else :
920
+ return False
0 commit comments