Skip to content

Commit b720979

Browse files
committed
Add new matchglob function and iterchildren method to paths
1 parent 4ef7d76 commit b720979

File tree

4 files changed

+403
-32
lines changed

4 files changed

+403
-32
lines changed

domdf_python_tools/paths.py

Lines changed: 132 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,16 @@
4141

4242
# stdlib
4343
import contextlib
44+
import fnmatch
4445
import gzip
4546
import json
4647
import os
4748
import pathlib
4849
import shutil
4950
import stat
5051
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
5254

5355
# this package
5456
from domdf_python_tools.typing import JsonLibrary, PathLike
@@ -69,7 +71,10 @@
6971
"WindowsPathPlus",
7072
"in_directory",
7173
"_P",
74+
"_PP",
7275
"traverse_to_file",
76+
"matchglob",
77+
"unwanted_dirs",
7378
]
7479

7580
newline_default = object()
@@ -81,6 +86,18 @@
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

85102
def 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

752796
class 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

Comments
 (0)