4141
4242# stdlib
4343import contextlib
44+ import fnmatch
4445import gzip
4546import json
4647import os
4748import pathlib
4849import shutil
4950import stat
5051import 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
5254
5355# this package
5456from domdf_python_tools .typing import JsonLibrary , PathLike
6971 "WindowsPathPlus" ,
7072 "in_directory" ,
7173 "_P" ,
74+ "_PP" ,
7275 "traverse_to_file" ,
76+ "matchglob" ,
77+ "unwanted_dirs" ,
7378 ]
7479
7580newline_default = object ()
8186.. versionchanged:: 1.7.0 Now bound to :class:`pathlib.Path`.
8287"""
8388
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+
84101
85102def append (var : str , filename : PathLike , ** kwargs ) -> int :
86103 """
@@ -167,7 +184,7 @@ def maybe_make(directory: PathLike, mode: int = 0o777, parents: bool = False):
167184 :param mode: Combined with the process’ umask value to determine the file mode and access flags
168185 :param parents: If :py:obj:`False` (the default), a missing parent raises a :class:`FileNotFoundError`.
169186 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).
171188 :no-default parents:
172189
173190 .. 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):
308325 """
309326 Subclass of :class:`pathlib.Path` with additional methods and a default encoding of UTF-8.
310327
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.
316334
317335 .. 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.
322337 """
323338
324339 __slots__ = ("_accessor" , )
@@ -380,7 +395,7 @@ def maybe_make(
380395
381396 .. versionchanged:: 1.6.0 Removed the ``'exist_ok'`` option, since it made no sense in this context.
382397
383- .. note ::
398+ .. attention ::
384399
385400 This will fail silently if a file with the same name already exists.
386401 This appears to be due to the behaviour of :func:`os.mkdir`.
@@ -562,9 +577,7 @@ def dump_json(
562577 rather than :meth:`PathPlus.write_text <domdf_python_tools.paths.PathPlus.write_text>`,
563578 and returns :py:obj:`None` rather than :class:`int`.
564579
565- .. versionchanged:: 1.9.0
566-
567- Added the ``compress`` keyword-only argument.
580+ .. versionchanged:: 1.9.0 Added the ``compress`` keyword-only argument.
568581 """
569582
570583 if compress :
@@ -602,9 +615,7 @@ def load_json(
602615
603616 :return: The deserialised JSON data.
604617
605- .. versionchanged:: 1.9.0
606-
607- Added the ``compress`` keyword-only argument.
618+ .. versionchanged:: 1.9.0 Added the ``compress`` keyword-only argument.
608619 """
609620
610621 if decompress :
@@ -676,12 +687,12 @@ def replace(self: _P, target: Union[str, pathlib.PurePath]) -> _P: # type: igno
676687
677688 Returns the new Path instance pointing to the target path.
678689
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+
679693 :param target:
680694
681695 :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
685696 """
686697
687698 self ._accessor .replace (self , target ) # type: ignore
@@ -723,12 +734,10 @@ def is_relative_to(self, *other: Union[str, os.PathLike]) -> bool:
723734 r"""
724735 Returns whether the path is relative to another path.
725736
726- :param \*other:
727-
728- :rtype:
729-
730737 .. versionadded:: 0.3.8 for Python 3.9 and above
731738 .. versionadded:: 1.4.0 for Python 3.6 and Python 3.7
739+
740+ :param \*other:
732741 """
733742
734743 try :
@@ -741,19 +750,54 @@ def abspath(self) -> "PathPlus":
741750 """
742751 Return the absolute version of the path.
743752
744- :rtype:
745-
746753 .. versionadded:: 1.3.0
747754 """
748755
749756 return self .__class__ (os .path .abspath (self ))
750757
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+
751795
752796class PosixPathPlus (PathPlus , pathlib .PurePosixPath ):
753797 """
754798 :class:`~.PathPlus` subclass for non-Windows systems.
755799
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.
757801
758802 .. versionadded:: 0.3.8
759803 """
@@ -765,7 +809,7 @@ class WindowsPathPlus(PathPlus, pathlib.PureWindowsPath):
765809 """
766810 :class:`~.PathPlus` subclass for Windows systems.
767811
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.
769813
770814 .. versionadded:: 0.3.8
771815 """
@@ -798,11 +842,11 @@ def traverse_to_file(base_directory: _P, *filename: PathLike, height: int = -1)
798842 r"""
799843 Traverse the parents of the given directory until the desired file is found.
800844
845+ .. versionadded:: 1.7.0
846+
801847 :param base_directory: The directory to start searching from
802848 :param \*filename: The filename(s) to search for
803849 :param height: The maximum height to traverse to.
804-
805- .. versionadded:: 1.7.0
806850 """
807851
808852 if not filename :
@@ -817,3 +861,60 @@ def traverse_to_file(base_directory: _P, *filename: PathLike, height: int = -1)
817861 return directory
818862
819863 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