This commit is contained in:
zy 2025-06-06 13:32:43 +02:00
parent f85db5191e
commit 48cd7869b2
4 changed files with 248 additions and 96 deletions

View file

@ -1,5 +1,6 @@
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("")

View file

@ -3,8 +3,7 @@ __add__ = [ "diff" ]
from collections.abc import Set
from os import listdir, readlink
from os.path import abspath
from typing import Callable, Final
from collections.abc import Iterable
from typing import Callable, Final, assert_never
from .mytypes import *
from .Config import *
@ -14,62 +13,125 @@ from .utils import *
class DiffResult:
def __init__(self, path:str) -> None:
self.path:Final[str] = path
self.differs:set[PathAndType] = set()
self.same:set[PathAndType] = set()
self.differentTypes:set[tuple[str,PathType,PathType]] = set()
self.presentOnlyInFirst:set[PathAndType] = set()
self.presentOnlyInSecond:set[PathAndType] = set()
self.weird:set[str] = set()
self.subresults:dict[str,DiffResult] = {}
"""
(see 'diff'.)
"""
def simplify(self, *, deep:bool) -> None:
for l in [
self.differs, self.same, self.presentOnlyInFirst,
self.presentOnlyInSecond
]:
mapSet(l, ( lambda v: ( v[0], simplifyPath(v[1]) ) ))
mapSet(self.differentTypes, (
lambda v: ( simplifyPath(v[0]), *v[1:] )
))
mapSet(self.weird, simplifyPath)
if deep:
for v in self.subresults.values():
v.simplify(deep=True)
def print(self, source:str|None = None, target:str|None = None) -> None:
reprSource = repr(source) if source is not None else "the source directory"
reprTarget = repr(target) if target is not None else "the target directory"
type RelPath = Path
"""
represents a path relative to the root of the two compared directories.
"""
def __init__(self, first:AbsPath, second:AbsPath, subpath:RelPath) -> None:
"""
(please create me using 'diff'.)
"""
self.first:Final[AbsPath] = first
"""
path of the first directory compared.
"""
self.second:Final[AbsPath] = second
"""
path of the second directory compared.
"""
self.subpath:Final[DiffResult.RelPath] = subpath
"""
self contains info from this directory.
"""
self.differs:set[NameAndType] = set()
"""
list the children of self.subpath that have a different content
(or link target for a symbolic link) in both directories.
"""
self.same:set[NameAndType] = set()
"""
list the children of self.subpath that have the same content
(or link target for a symbolic link) in both directories.
"""
self.differentTypes:set[tuple[Filename,PathType,PathType]] = set()
"""
list the children of self.subpath that have a different PathType
in both directories.
self.differentTypes[1] is the PathType of the one
in the firstdirectory, self.differentTypes[2] is the PathType
of the one in the second directory.
"""
self.presentOnlyInFirst:set[NameAndType] = set()
"""
list the children of self.subpath that are present
only in the first directory.
"""
self.presentOnlyInSecond:set[NameAndType] = set()
"""
list the children of self.subpath that are present
only in the second directory.
"""
self.weird:set[tuple[Filename,PathTypeW|None,PathTypeW|None]] = set()
"""
list the children that have an unknown type in at least one
of the two directories. They are ignored whe comparing two directories.
self.weird[1] is the type of the one
in the firstdirectory, self.weird[2] is the type
of the one in the second directory. 'None' means the file doesn't exist
in the directory.
"""
self.subresults:dict[Filename,DiffResult] = {}
"""
for any directory in self.differs there is a corresponding DiffResult.
"""
def print(self) -> None:
"""
print a complete representation of self.
"""
def reprPathAndType(v:PathAndType) -> str:
return f"{lineForPathAndType(v)}"
def printWith[T](label:str, what:set[T], f:Callable[[T],str]):
if len(what) == 0:
return
print(label + ":")
for v in what:
print(f"-\t{f(v)}")
title(f"for path {repr(self.path)}")
printWith("same", self.same, reprPathAndType)
title(f"comparaison of the two directories")
title(f"- {self.first}")
title(f"- {self.second}")
if self.subpath != "/":
title(f"in the subdirectory {repr(self.subpath)}")
print()
printWith("different", self.differs, reprPathAndType)
printWith("same", self.same, reprFileAndType)
printWith("different", self.differs, reprFileAndType)
printWith("different types", self.differentTypes,
lambda v: f"{reprPathType(v[1])} / {reprPathType(v[2])}: {v[0]}"
)
printWith(f"exist only in {reprSource}",
self.presentOnlyInFirst, reprPathAndType
printWith(f"exists only in {self.first}",
self.presentOnlyInFirst, reprFileAndType
)
printWith(f"exist only in {reprTarget}",
self.presentOnlyInSecond, reprPathAndType
printWith(f"exists only in {self.second}",
self.presentOnlyInSecond, reprFileAndType
)
printWith("dunno what that is lol", self.weird,
lambda v: v
printWith("unknown types", self.weird,
lambda v: f"{reprPathType(v[1])} / {reprPathType(v[2])}: {v[0]}"
)
@ -79,84 +141,92 @@ def diff(pathA:str, pathB:str, *, ignored:Set[PathAndType]) -> DiffResult:
def _diffDirs(
absPathA:str, absPathB:str, absIgnoredPaths:Set[PathAndType],
relCurrentPath:str, *, trust:bool
pathA:AbsPath, pathB:AbsPath, ignored:Set[PathAndType],
currentPath:DiffResult.RelPath, *, trust:bool
) -> DiffResult:
absCurrentPathA = absPathA + "/" + relCurrentPath
absCurrentPathB = absPathB + "/" + relCurrentPath
absCurrentPathA_what = whatsthispath(absCurrentPathA)
absCurrentPathB_what = whatsthispath(absCurrentPathB)
currentPathA = pathA + "/" + currentPath
currentPathB = pathB + "/" + currentPath
currentPathA_type = whatsthispath(currentPathA)
currentPathB_type = whatsthispath(currentPathB)
if not trust:
assert absPathA[0] == "/", "'pathA' should be an absolute path."
assert absPathB[0] == "/", "'pathB' should be an absolute path."
assert relCurrentPath[0] == "/", (
"'relCurrentPath' should start with '/'."
assert pathA[0] == "/", "'pathA' shall be an absolute path."
assert pathB[0] == "/", "'pathB' shall be an absolute path."
assert currentPath[0] == "/", (
"'currentPath' shall start with '/'."
)
assert all([ v[1][0] == "/" for v in absIgnoredPaths ]), (
"'ignoredPaths' must contain only absolute paths."
assert all([ v[1][0] == "/" for v in ignored ]), (
"'ignoredPaths' shall contain only absolute paths."
)
assert absCurrentPathA_what == 'd', "'pathA' should be a directory."
assert absCurrentPathB_what == 'd', "'pathB' should be a directory."
assert currentPathA_type == 'd', "'pathA' shall be a directory."
assert currentPathB_type == 'd', "'pathB' shall be a directory."
r = DiffResult(relCurrentPath)
r = DiffResult(pathA, pathB, currentPath)
for name in set(listdir(absCurrentPathA)) | set(listdir(absCurrentPathB)):
relNewPath = relCurrentPath + "/" + name
absNewPathA = absCurrentPathA + "/" + name
absNewPathB = absCurrentPathB + "/" + name
absNewPathA_what = whatsthispath(absNewPathA)
absNewPathB_what = whatsthispath(absNewPathB)
if absNewPathA_what is None:
assert absNewPathB_what is not None
r.presentOnlyInSecond.add((absNewPathB_what, relNewPath))
elif absNewPathB_what is None:
r.presentOnlyInFirst.add((absNewPathA_what, relNewPath))
elif absNewPathA_what != absNewPathB_what:
for name in set(listdir(currentPathA)) | set(listdir(currentPathB)):
newPath:DiffResult.RelPath = currentPath + "/" + name
newPathA:AbsPath = currentPathA + "/" + name
newPathB:AbsPath = currentPathB + "/" + name
newPathA_type = whatsthispath(newPathA)
newPathB_type = whatsthispath(newPathB)
if newPathA_type == "w" or newPathB_type == "w":
# file is weird
r.weird.add(
(name, newPathA_type, newPathB_type)
)
elif newPathA_type is None:
# file doesnt exist in first
assert newPathB_type is not None
r.presentOnlyInSecond.add((newPathB_type, name))
elif newPathB_type is None:
# file doesnt exist in second
r.presentOnlyInFirst.add((newPathA_type, name))
elif newPathA_type != newPathB_type:
# file types are different
r.differentTypes.add(
(relNewPath, absNewPathA_what, absNewPathB_what)
(name, newPathA_type, newPathB_type)
)
else:
# file type is the same
_diffOne(r,
absPathA, absPathB, absIgnoredPaths, relNewPath, name,
absNewPathA_what
pathA, pathB, ignored, newPath, name,
newPathA_type
)
r.simplify(deep=False)
return r
def _diffOne(r:DiffResult, absPathA:str, absPathB:str, absIgnoredPaths:Set[PathAndType],
relCurrentPath:str, filename:str, relCurrentPath_what:PathType
def _diffOne(r:DiffResult, pathA:AbsPath, pathB:AbsPath,
ignored:Set[PathAndType], path:DiffResult.RelPath, filename:str,
what:PathType
) -> None:
match relCurrentPath_what:
match what:
case "d":
rr = _diffDirs(absPathA, absPathB, absIgnoredPaths, relCurrentPath, trust=False) # TODO(debug) add trust=True
# files are directories
rr = _diffDirs(pathA, pathB, ignored, path, trust=False) # TODO(debug) add trust=True
r.subresults[filename] = rr
if (len(rr.differentTypes) > 0
or len(rr.presentOnlyInFirst) > 0
or len(rr.presentOnlyInSecond) > 0
or len(rr.differs) > 0
):
r.differs.add( (relCurrentPath_what, filename) )
elif len(rr.weird) > 0:
r.weird.add( filename )
r.differs.add( (what, filename) )
else:
r.same.add( ( relCurrentPath_what, filename ) )
r.same.add( ( what, filename ) )
case "f"|"l":
getData:Callable[[str],str|bytes] = (
(lambda path: open(path, "rb").read())
if relCurrentPath_what == "f" else
(lambda path: readlink(path))
)
def getContent(path:str) -> str|bytes:
nonlocal what
if what == "l":
return readlink(path)
return open(path, "rb").read()
(
r.same if (
getData(absPathA + "/" + relCurrentPath)
== getData(absPathB + "/" + relCurrentPath)
getContent(pathA + "/" + path)
== getContent(pathB + "/" + path)
) else r.differs
).add( (relCurrentPath_what, filename) )
).add( (what, filename) )
case "w":
r.weird.add( filename )
case _:
assert_never(what)

View file

@ -1,16 +1,90 @@
__all__ = [
"PathType",
"PathAndType",
"PathTypeW",
"Filename",
"Path",
"AbsPath",
"NameAndType",
"NameAndTypeW",
"reprPathType",
"lineForPathAndType",
"reprFileAndType",
"PathAndType",
]
from typing import Literal
type PathType = Literal["d","f","l","w"]
type PathType = Literal["d","f","l"]
"""
All the types that a known element can have:
- 'd': directory
- 'f': file
- 'l': symbolic link
"""
def reprPathType(t:PathType) -> str:
type PathTypeW = PathType|Literal["w"]
"""
All the types that an element can have.
To use when the type of an element can be not in PathType.
Superset of PathType.
'w' (weird) means the type is unknown.
"""
##
## These types are just aliases to str, but they allow to define
## what a variable can contain.
##
type Filename = str
"""
represents the name of a file, a directory or whatever else.
can't contain any '/' obviously.
"""
type Path = str
"""
represents any path.
must start with '/'.
"""
type AbsPath = Path
"""
represents a Path relative to the linux root.
"""
type NameAndType = tuple[PathType,Filename]
"""
a Filename and its associated PathType.
"""
type NameAndTypeW = tuple[PathTypeW,Filename]
"""
a Filename and its associated PathTypeW.
"""
type PathAndType = tuple[PathType,Filename]
"""
a Path and its associated PathType.
"""
type PathAndTypeW = tuple[PathTypeW,Filename]
"""
a Path and its associated PathType.
"""
def reprPathType(t:PathTypeW|None) -> str:
"""
return a common name in natural language of the given type.
t == None means the file doesn't exist.
"""
match t:
case "d":
return "directory"
@ -20,9 +94,11 @@ def reprPathType(t:PathType) -> str:
return "symbolic link"
case "w":
return "unknown type"
case None:
return "(doesn't exist)"
type PathAndType = tuple["PathType",str]
def lineForPathAndType(v:PathAndType) -> str:
def reprFileAndType(v:NameAndTypeW|PathAndTypeW) -> str:
"""
equivalent to repr() for a NameAndTypeW.
"""
return f"{reprPathType(v[0])}: {v[1]}"

View file

@ -5,7 +5,12 @@ import os.path
from .mytypes import *
def whatsthispath(path:str) -> PathType|None:
def whatsthispath(path:AbsPath) -> PathTypeW|None:
"""
get the PathTypeW of a file.
if the file doesn't exist, returns None.
"""
if os.path.islink(path):
return "l"
if os.path.isdir(path):