diff --git a/dictdiffer/__init__.pyi b/dictdiffer/__init__.pyi new file mode 100644 index 0000000..734b4e2 --- /dev/null +++ b/dictdiffer/__init__.pyi @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +from typing import ( + Hashable, + Iterable, + Iterator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, +) + +from typing_extensions import Literal, TypeAlias + +_DiffPath: TypeAlias = Union[str, List[object]] +_DiffPair: TypeAlias = Tuple[object, object] +_AddDiff: TypeAlias = Tuple[Literal["add"], _DiffPath, List[_DiffPair]] +_RemoveDiff: TypeAlias = Tuple[Literal["remove"], _DiffPath, List[_DiffPair]] +_ChangeDiff: TypeAlias = Tuple[Literal["change"], _DiffPath, _DiffPair] +_Diff: TypeAlias = Union[_AddDiff, _RemoveDiff, _ChangeDiff] +_PathTuple: TypeAlias = Tuple[Hashable, ...] + +_DestinationT = TypeVar("_DestinationT") + +from . import utils as utils +from . import version as version +from .utils import EPSILON, PathLimit, are_different +from .utils import dot_lookup as dot_lookup +from .version import __version__ as __version__ + +ADD: Literal["add"] +REMOVE: Literal["remove"] +CHANGE: Literal["change"] +DICT_TYPES: Tuple[Type[object], ...] +LIST_TYPES: Tuple[Type[object], ...] +SET_TYPES: Tuple[Type[object], ...] +HAS_NUMPY: bool +__all__ = ("diff", "patch", "swap", "revert", "dot_lookup", "__version__") + +def diff( + first: object, + second: object, + node: Optional[List[object]] = ..., + ignore: Optional[Iterable[object]] = ..., + path_limit: Optional[Union[PathLimit, Iterable[Iterable[Hashable]]]] = ..., + expand: bool = ..., + tolerance: Optional[float] = ..., + absolute_tolerance: Optional[float] = ..., + dot_notation: bool = ..., +) -> Iterator[_Diff]: ... +def patch( + diff_result: Iterable[_Diff], + destination: _DestinationT, + in_place: bool = ..., +) -> _DestinationT: ... +def swap(diff_result: Iterable[_Diff]) -> Iterator[_Diff]: ... +def revert( + diff_result: Iterable[_Diff], + destination: _DestinationT, + in_place: bool = ..., +) -> _DestinationT: ... diff --git a/dictdiffer/conflict.pyi b/dictdiffer/conflict.pyi new file mode 100644 index 0000000..8fbf0e3 --- /dev/null +++ b/dictdiffer/conflict.pyi @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +from typing import Iterable, List, Optional + +from typing_extensions import Literal + +from . import _Diff +from .utils import get_path as get_path +from .utils import is_super_path as is_super_path + +class Conflict: + first_patch: _Diff + second_patch: _Diff + take: Optional[Literal["f", "s"]] + def __init__(self, patch1: _Diff, patch2: _Diff) -> None: ... + def take_patch(self) -> _Diff: ... + def __repr__(self) -> str: ... + +class ConflictFinder: + conflicts: List[Conflict] + def find_conflicts( + self, + first_patches: Iterable[_Diff], + second_patches: Iterable[_Diff], + ) -> List[Conflict]: ... diff --git a/dictdiffer/merge.pyi b/dictdiffer/merge.pyi new file mode 100644 index 0000000..3f26acc --- /dev/null +++ b/dictdiffer/merge.pyi @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +from typing import ( + Generic, + Hashable, + Iterable, + List, + Mapping, + Optional, + Sequence, + TypeVar, + overload, +) + +from typing_extensions import Literal + +from . import _Diff, _PathTuple +from . import diff as diff +from .conflict import Conflict +from .conflict import ConflictFinder as ConflictFinder +from .resolve import Resolver as Resolver +from .resolve import UnresolvedConflictsException as UnresolvedConflictsException +from .resolve import _ResolutionAction +from .unify import Unifier as Unifier +from .utils import PathLimit as PathLimit + +_DataT = TypeVar("_DataT") +_InfoT = TypeVar("_InfoT") +_ConstructorDataT = TypeVar("_ConstructorDataT") +_ConstructorInfoT = TypeVar("_ConstructorInfoT") + +class Merger(Generic[_DataT, _InfoT]): + lca: _DataT + first: _DataT + second: _DataT + path_limit: PathLimit + ignore: Optional[Iterable[object]] + actions: Mapping[_PathTuple, _ResolutionAction[_InfoT]] + additional_info: _InfoT + conflict_finder: ConflictFinder + resolver: Resolver[_InfoT] + unifier: Unifier + conflicts: List[Conflict] + unresolved_conflicts: List[Conflict] + first_patches: List[_Diff] + second_patches: List[_Diff] + unified_patches: List[_Diff] + + @overload + def __init__( + self: Merger[_ConstructorDataT, None], + lca: _ConstructorDataT, + first: _ConstructorDataT, + second: _ConstructorDataT, + actions: Mapping[_PathTuple, _ResolutionAction[None]], + path_limits: Iterable[Iterable[Hashable]] = ..., + additional_info: None = ..., + ignore: Optional[Iterable[object]] = ..., + ) -> None: ... + @overload + def __init__( + self: Merger[_ConstructorDataT, _ConstructorInfoT], + lca: _ConstructorDataT, + first: _ConstructorDataT, + second: _ConstructorDataT, + actions: Mapping[_PathTuple, _ResolutionAction[_ConstructorInfoT]], + path_limits: Iterable[Iterable[Hashable]] = ..., + additional_info: _ConstructorInfoT = ..., + ignore: Optional[Iterable[object]] = ..., + ) -> None: ... + def run(self) -> None: ... + def continue_run(self, picks: Sequence[Literal["f", "s"]]) -> None: ... + def extract_patches(self) -> None: ... + def find_conflicts(self) -> None: ... + def resolve_conflicts(self) -> None: ... + def unify_patches(self) -> None: ... diff --git a/dictdiffer/py.typed b/dictdiffer/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/dictdiffer/resolve.pyi b/dictdiffer/resolve.pyi new file mode 100644 index 0000000..db93dfb --- /dev/null +++ b/dictdiffer/resolve.pyi @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +from typing import Callable, Generic, List, Mapping, Sequence, TypeVar, overload + +from typing_extensions import Literal, TypeAlias + +from . import _Diff, _PathTuple +from .conflict import Conflict +from .utils import get_path as get_path + +_InfoT = TypeVar("_InfoT") +_ResolutionAction: TypeAlias = Callable[ + [Conflict, List[_Diff], List[_Diff], _InfoT], + bool, +] + +class UnresolvedConflictsException(Exception): + message: str + content: List[Conflict] + def __init__(self, unresolved_conflicts: List[Conflict]) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +class NoFurtherResolutionException(Exception): ... + +class Resolver(Generic[_InfoT]): + actions: Mapping[_PathTuple, _ResolutionAction[_InfoT]] + additional_info: _InfoT + unresolved_conflicts: List[Conflict] + + @overload + def __init__( + self: Resolver[None], + actions: Mapping[_PathTuple, _ResolutionAction[None]], + additional_info: None = ..., + ) -> None: ... + @overload + def __init__( + self, + actions: Mapping[_PathTuple, _ResolutionAction[_InfoT]], + additional_info: _InfoT, + ) -> None: ... + def resolve_conflicts( + self, + first_patches: List[_Diff], + second_patches: List[_Diff], + conflicts: Sequence[Conflict], + ) -> None: ... + def manual_resolve_conflicts( + self, + picks: Sequence[Literal["f", "s"]], + ) -> None: ... diff --git a/dictdiffer/testing.pyi b/dictdiffer/testing.pyi new file mode 100644 index 0000000..9fb47ff --- /dev/null +++ b/dictdiffer/testing.pyi @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +from typing import Hashable, Iterable, List, Optional, Union + +from . import diff as diff +from .utils import PathLimit + +def assert_no_diff( + first: object, + second: object, + node: Optional[List[object]] = ..., + ignore: Optional[Iterable[object]] = ..., + path_limit: Optional[Union[PathLimit, Iterable[Iterable[Hashable]]]] = ..., + expand: bool = ..., + tolerance: Optional[float] = ..., + absolute_tolerance: Optional[float] = ..., + dot_notation: bool = ..., +) -> None: ... diff --git a/dictdiffer/unify.pyi b/dictdiffer/unify.pyi new file mode 100644 index 0000000..c181912 --- /dev/null +++ b/dictdiffer/unify.pyi @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +from typing import Iterable, List + +from . import _Diff +from .conflict import Conflict +from .utils import get_path as get_path +from .utils import nested_hash as nested_hash + +class Unifier: + unified_patches: List[_Diff] + def unify( + self, + first_patches: List[_Diff], + second_patches: List[_Diff], + conflicts: Iterable[Conflict], + ) -> List[_Diff]: ... diff --git a/dictdiffer/utils.pyi b/dictdiffer/utils.pyi new file mode 100644 index 0000000..cd86b28 --- /dev/null +++ b/dictdiffer/utils.pyi @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +from typing import ( + Dict, + Generic, + Hashable, + Iterable, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +from typing_extensions import Literal, TypeAlias + +from . import _Diff, _DiffPair, _DiffPath, _PathTuple + +_ValueT = TypeVar("_ValueT") +_SourceT = TypeVar("_SourceT") +_PathPatch: TypeAlias = Tuple[str, _DiffPath, Sequence[_DiffPair]] +_PathLimitTree: TypeAlias = Dict[ + Hashable, + Union[bool, "_PathLimitTree"], +] + +num_types: Tuple[Type[int], Type[float]] +EPSILON: float + +class WildcardDict(Dict[_PathTuple, _ValueT], Generic[_ValueT]): + star_keys: Set[_PathTuple] + plus_keys: Set[_PathTuple] + + @overload + def __init__(self, values: None = ...) -> None: ... + @overload + def __init__(self, values: Mapping[_PathTuple, _ValueT]) -> None: ... + def __getitem__(self, key: _PathTuple) -> _ValueT: ... + def __setitem__(self, key: _PathTuple, value: _ValueT) -> None: ... + def query_path(self, key: _PathTuple) -> _PathTuple: ... + +class PathLimit: + final_key: Hashable + dict: _PathLimitTree + def __init__( + self, + path_limits: Iterable[Iterable[Hashable]] = ..., + final_key: Optional[Hashable] = ..., + ) -> None: ... + def path_is_limit(self, key_path: Iterable[Hashable]) -> bool: ... + +def create_dotted_node(node: Sequence[object]) -> Union[str, List[object]]: ... +def get_path(patch: Union[_Diff, _PathPatch]) -> Tuple[object, ...]: ... +def is_super_path(path1: Iterable[object], path2: Iterable[object]) -> bool: ... +def nested_hash(obj: object) -> Optional[int]: ... + +@overload +def dot_lookup( + source: _SourceT, + lookup: None, + parent: bool = ..., +) -> _SourceT: ... +@overload +def dot_lookup( + source: _SourceT, + lookup: Literal[""], + parent: bool = ..., +) -> _SourceT: ... +@overload +def dot_lookup( + source: object, + lookup: Union[str, List[object]], + parent: bool = ..., +) -> object: ... + +def are_different( + first: object, + second: object, + tolerance: Optional[float], + absolute_tolerance: Optional[float] = ..., +) -> bool: ... diff --git a/dictdiffer/version.pyi b/dictdiffer/version.pyi new file mode 100644 index 0000000..ac16078 --- /dev/null +++ b/dictdiffer/version.pyi @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 HeeJae Chang. +# SPDX-License-Identifier: MIT + +__version__: str