|
12 | 12 | #
|
13 | 13 | # Copyright © 2018-2020 Dominic Davis-Foster <[email protected]>
|
14 | 14 | #
|
15 |
| -# Parts of the docstrings and the PathPlus class based on the Python 3.8.2 Documentation |
| 15 | +# Parts of the docstrings, the PathPlus class and the DirComparator class |
| 16 | +# based on Python and its Documentation |
16 | 17 | # Licensed under the Python Software Foundation License Version 2.
|
17 |
| -# Copyright © 2001-2020 Python Software Foundation. All rights reserved. |
| 18 | +# Copyright © 2001-2021 Python Software Foundation. All rights reserved. |
18 | 19 | # Copyright © 2000 BeOpen.com. All rights reserved.
|
19 | 20 | # Copyright © 1995-2000 Corporation for National Research Initiatives. All rights reserved.
|
20 | 21 | # Copyright © 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
|
|
41 | 42 |
|
42 | 43 | # stdlib
|
43 | 44 | import contextlib
|
| 45 | +import filecmp |
44 | 46 | import fnmatch
|
45 | 47 | import gzip
|
46 | 48 | import json
|
|
52 | 54 | import tempfile
|
53 | 55 | from collections import defaultdict, deque
|
54 | 56 | from operator import methodcaller
|
55 |
| -from typing import IO, Any, Callable, ContextManager, Dict, Iterable, Iterator, List, Optional, TypeVar, Union |
| 57 | +from typing import ( |
| 58 | + IO, |
| 59 | + Any, |
| 60 | + Callable, |
| 61 | + ContextManager, |
| 62 | + Dict, |
| 63 | + Iterable, |
| 64 | + Iterator, |
| 65 | + List, |
| 66 | + Optional, |
| 67 | + Sequence, |
| 68 | + TypeVar, |
| 69 | + Union |
| 70 | + ) |
56 | 71 |
|
57 | 72 | # this package
|
58 | 73 | from domdf_python_tools.compat import nullcontext
|
|
80 | 95 | "unwanted_dirs",
|
81 | 96 | "TemporaryPathPlus",
|
82 | 97 | "sort_paths",
|
| 98 | + "DirComparator", |
| 99 | + "compare_dirs", |
83 | 100 | ]
|
84 | 101 |
|
85 | 102 | newline_default = object()
|
@@ -330,12 +347,12 @@ class PathPlus(pathlib.Path):
|
330 | 347 | """
|
331 | 348 | Subclass of :class:`pathlib.Path` with additional methods and a default encoding of UTF-8.
|
332 | 349 |
|
333 |
| - Path represents a filesystem path but unlike :class:`~.PurePath`, also offers |
| 350 | + Path represents a filesystem path but unlike :class:`pathlib.PurePath`, also offers |
334 | 351 | methods to do system calls on path objects.
|
335 | 352 | Depending on your system, instantiating a :class:`~.PathPlus` will return
|
336 | 353 | either a :class:`~.PosixPathPlus` or a :class:`~.WindowsPathPlus`. object.
|
337 |
| - You can also instantiate a :class:`PosixPath` or :class:`WindowsPath` directly, |
338 |
| - but cannot instantiate a :class:`WindowsPath` on a POSIX system or vice versa. |
| 354 | + You can also instantiate a :class:`~.PosixPathPlus` or :class:`WindowsPath` directly, |
| 355 | + but cannot instantiate a :class:`~.WindowsPathPlus` on a POSIX system or vice versa. |
339 | 356 |
|
340 | 357 | .. versionadded:: 0.3.8
|
341 | 358 | .. versionchanged:: 0.5.1 Defaults to Unix line endings (``LF``) on all platforms.
|
@@ -1037,3 +1054,93 @@ def sort_paths(*paths: PathLike) -> List[PathPlus]:
|
1037 | 1054 | files.extend(PathPlus(directory) / path for path in sort_paths(*contents))
|
1038 | 1055 |
|
1039 | 1056 | return files + sorted(local_contents, key=methodcaller("as_posix"))
|
| 1057 | + |
| 1058 | + |
| 1059 | +class DirComparator(filecmp.dircmp): |
| 1060 | + r""" |
| 1061 | + Compare the content of ``a`` and ``a``. |
| 1062 | +
|
| 1063 | + In contrast with :class:`filecmp.dircmp`, this |
| 1064 | + subclass compares the content of files with the same path. |
| 1065 | +
|
| 1066 | + .. versionadded:: 2.7.0 |
| 1067 | +
|
| 1068 | + :param a: The "left" directory to compare. |
| 1069 | + :param b: The "right" directory to compare. |
| 1070 | + :param ignore: A list of names to ignore. |
| 1071 | + :default ignore: :py:obj:`filecmp.DEFAULT_IGNORES` |
| 1072 | + :param hide: A list of names to hide. |
| 1073 | + :default hide: ``[`` :py:obj:`os.curdir`, :py:obj:`os.pardir` ``]`` |
| 1074 | + """ |
| 1075 | + |
| 1076 | + # From https://stackoverflow.com/a/24860799, public domain. |
| 1077 | + # Thanks Philippe |
| 1078 | + |
| 1079 | + def __init__( |
| 1080 | + self, |
| 1081 | + a: PathLike, |
| 1082 | + b: PathLike, |
| 1083 | + ignore: Optional[Sequence[str]] = None, |
| 1084 | + hide: Optional[Sequence[str]] = None, |
| 1085 | + ): |
| 1086 | + super().__init__(a, b, ignore=ignore, hide=hide) |
| 1087 | + |
| 1088 | + def phase3(self) -> None: |
| 1089 | + # Find out differences between common files. |
| 1090 | + # Ensure we are using content comparison with shallow=False. |
| 1091 | + |
| 1092 | + fcomp = filecmp.cmpfiles(self.left, self.right, self.common_files, shallow=False) |
| 1093 | + self.same_files, self.diff_files, self.funny_files = fcomp |
| 1094 | + |
| 1095 | + def phase4(self) -> None: |
| 1096 | + # Find out differences between common subdirectories |
| 1097 | + |
| 1098 | + # From https://github.com/python/cpython/pull/23424 |
| 1099 | + |
| 1100 | + self.subdirs = {} |
| 1101 | + |
| 1102 | + for x in self.common_dirs: |
| 1103 | + a_x = os.path.join(self.left, x) |
| 1104 | + b_x = os.path.join(self.right, x) |
| 1105 | + self.subdirs[x] = self.__class__(a_x, b_x, self.ignore, self.hide) |
| 1106 | + |
| 1107 | + _methodmap = { |
| 1108 | + "subdirs": phase4, |
| 1109 | + "same_files": phase3, |
| 1110 | + "diff_files": phase3, |
| 1111 | + "funny_files": phase3, |
| 1112 | + "common_dirs": filecmp.dircmp.phase2, |
| 1113 | + "common_files": filecmp.dircmp.phase2, |
| 1114 | + "common_funny": filecmp.dircmp.phase2, |
| 1115 | + "common": filecmp.dircmp.phase1, |
| 1116 | + "left_only": filecmp.dircmp.phase1, |
| 1117 | + "right_only": filecmp.dircmp.phase1, |
| 1118 | + "left_list": filecmp.dircmp.phase0, |
| 1119 | + "right_list": filecmp.dircmp.phase0 |
| 1120 | + } |
| 1121 | + |
| 1122 | + methodmap = _methodmap # type: ignore |
| 1123 | + |
| 1124 | + |
| 1125 | +def compare_dirs(a: PathLike, b: PathLike): |
| 1126 | + """ |
| 1127 | + Compare the content of two directory trees. |
| 1128 | +
|
| 1129 | + .. versionadded:: 2.7.0 |
| 1130 | +
|
| 1131 | + :param a: The "left" directory to compare. |
| 1132 | + :param b: The "right" directory to compare. |
| 1133 | +
|
| 1134 | + :returns: :py:obj:`False` if they differ, :py:obj:`True` is they are the same. |
| 1135 | + """ |
| 1136 | + |
| 1137 | + compared = DirComparator(a, b) |
| 1138 | + |
| 1139 | + if compared.left_only or compared.right_only or compared.diff_files or compared.funny_files: |
| 1140 | + return False |
| 1141 | + |
| 1142 | + for subdir in compared.common_dirs: |
| 1143 | + if not compare_dirs(os.path.join(a, subdir), os.path.join(b, subdir)): |
| 1144 | + return False |
| 1145 | + |
| 1146 | + return True |
0 commit comments