Skip to content

Commit e34199a

Browse files
committed
Add functions for comparing directories to "paths"
1 parent 734e235 commit e34199a

File tree

3 files changed

+372
-7
lines changed

3 files changed

+372
-7
lines changed

doc-source/api/paths.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
.. automodule:: domdf_python_tools.paths
66
:undoc-members:
7-
:exclude-members: __enter__,__exit__,__slots__
7+
:exclude-members: __enter__,__exit__,__slots__,methodmap,phase3,phase4

domdf_python_tools/paths.py

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
#
1313
# Copyright © 2018-2020 Dominic Davis-Foster <[email protected]>
1414
#
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
1617
# 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.
1819
# Copyright © 2000 BeOpen.com. All rights reserved.
1920
# Copyright © 1995-2000 Corporation for National Research Initiatives. All rights reserved.
2021
# Copyright © 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
@@ -41,6 +42,7 @@
4142

4243
# stdlib
4344
import contextlib
45+
import filecmp
4446
import fnmatch
4547
import gzip
4648
import json
@@ -52,7 +54,20 @@
5254
import tempfile
5355
from collections import defaultdict, deque
5456
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+
)
5671

5772
# this package
5873
from domdf_python_tools.compat import nullcontext
@@ -80,6 +95,8 @@
8095
"unwanted_dirs",
8196
"TemporaryPathPlus",
8297
"sort_paths",
98+
"DirComparator",
99+
"compare_dirs",
83100
]
84101

85102
newline_default = object()
@@ -330,12 +347,12 @@ class PathPlus(pathlib.Path):
330347
"""
331348
Subclass of :class:`pathlib.Path` with additional methods and a default encoding of UTF-8.
332349
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
334351
methods to do system calls on path objects.
335352
Depending on your system, instantiating a :class:`~.PathPlus` will return
336353
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.
339356
340357
.. versionadded:: 0.3.8
341358
.. versionchanged:: 0.5.1 Defaults to Unix line endings (``LF``) on all platforms.
@@ -1037,3 +1054,93 @@ def sort_paths(*paths: PathLike) -> List[PathPlus]:
10371054
files.extend(PathPlus(directory) / path for path in sort_paths(*contents))
10381055

10391056
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

Comments
 (0)