diff --git a/MANIFEST.in b/MANIFEST.in
index b1fc69e..e69de29 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +0,0 @@
-include VERSION
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 24050eb..be3c01d 100644
--- a/README.rst
+++ b/README.rst
@@ -2,17 +2,28 @@
deepmerge
=========
+.. image:: https://img.shields.io/pypi/v/deepmerge.svg
+ :target: https://pypi.org/project/deepmerge/
+
+.. image:: https://img.shields.io/pypi/status/deepmerge.svg
+ :target: https://pypi.org/project/deepmerge/
+
+.. image:: https://img.shields.io/pypi/pyversions/pillar.svg
+ :target: https://github.com/toumorokoshi/deepmerge
+
+.. image:: https://img.shields.io/github/license/toumorokoshi/deepmerge.svg
+ :target: https://github.com/toumorokoshi/deepmerge
+
.. image:: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml/badge.svg
:target: https://github.com/toumorokoshi/deepmerge/actions/workflows/python-package.yaml
-A tools to handle merging of
-nested data structures in python.
+A tool to handle merging of nested data structures in Python.
------------
Installation
------------
-deepmerge is available on `pypi `_:
+deepmerge is available on `pypi `_:
.. code-block:: bash
@@ -67,4 +78,4 @@ Example
You can also pass in your own merge functions, instead of a string.
-For more information, see the `docs `_
\ No newline at end of file
+For more information, see the `docs `_
diff --git a/deepmerge/__init__.py b/deepmerge/__init__.py
index b7686cf..d6477fe 100644
--- a/deepmerge/__init__.py
+++ b/deepmerge/__init__.py
@@ -1,9 +1,11 @@
+from typing import List, Tuple, Type
+
from .merger import Merger
from .strategy.core import STRATEGY_END # noqa
# some standard mergers available
-DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES = [
+DEFAULT_TYPE_SPECIFIC_MERGE_STRATEGIES: List[Tuple[Type, str]] = [
(list, "append"),
(dict, "merge"),
(set, "union"),
diff --git a/deepmerge/compat.py b/deepmerge/compat.py
deleted file mode 100644
index d2872a6..0000000
--- a/deepmerge/compat.py
+++ /dev/null
@@ -1,4 +0,0 @@
-try:
- string_type = basestring
-except:
- string_type = str
diff --git a/deepmerge/exception.py b/deepmerge/exception.py
index ea36dac..fd1902c 100644
--- a/deepmerge/exception.py
+++ b/deepmerge/exception.py
@@ -1,18 +1,35 @@
+from __future__ import annotations
+
+from typing import List, Any
+
+import deepmerge.merger
+
+
class DeepMergeException(Exception):
- pass
+ "Base class for all `deepmerge` Exceptions"
class StrategyNotFound(DeepMergeException):
- pass
+ "Exception for when a strategy cannot be located"
class InvalidMerge(DeepMergeException):
- def __init__(self, strategy_list_name, merge_args, merge_kwargs):
- super(InvalidMerge, self).__init__(
- "no more strategies found for {0} and arguments {1}, {2}".format(
- strategy_list_name, merge_args, merge_kwargs
- )
+ "Exception for when unable to complete a merge operation"
+
+ def __init__(
+ self,
+ strategy_list_name: str,
+ config: deepmerge.merger.Merger,
+ path: List,
+ base: Any,
+ nxt: Any,
+ ) -> None:
+ super().__init__(
+ f"Could not merge using {strategy_list_name!r} [{config=}, {path=}, {base=}, {nxt=}]"
)
self.strategy_list_name = strategy_list_name
- self.merge_args = merge_args
- self.merge_kwargs = merge_kwargs
+ self.config = config
+ self.path = path
+ self.base = base
+ self.nxt = nxt
+ return
diff --git a/deepmerge/extended_set.py b/deepmerge/extended_set.py
index 1d51b43..3efb399 100644
--- a/deepmerge/extended_set.py
+++ b/deepmerge/extended_set.py
@@ -1,3 +1,6 @@
+from typing import Sequence, Any
+
+
class ExtendedSet(set):
"""
ExtendedSet is an extension of set, which allows for usage
@@ -9,17 +12,17 @@ class ExtendedSet(set):
- unhashable types
"""
- def __init__(self, elements):
- self._values_by_hash = {self._hash(e): e for e in elements}
+ def __init__(self, elements: Sequence) -> None:
+ self._values_by_hash = {self._hash_element(e): e for e in elements}
- def _insert(self, element):
- self._values_by_hash[self._hash(element)] = element
+ def _insert(self, element: Any) -> None:
+ self._values_by_hash[self._hash_element(element)] = element
+ return
- def _hash(self, element):
+ def _hash_element(self, element: Any) -> int:
if getattr(element, "__hash__") is not None:
return hash(element)
- else:
- return hash(str(element))
+ return hash(str(element))
- def __contains__(self, obj):
- return self._hash(obj) in self._values_by_hash
+ def __contains__(self, obj: Any) -> bool:
+ return self._hash_element(obj) in self._values_by_hash
diff --git a/deepmerge/merger.py b/deepmerge/merger.py
index 5ccd568..4bd3455 100644
--- a/deepmerge/merger.py
+++ b/deepmerge/merger.py
@@ -1,44 +1,66 @@
-from .strategy.list import ListStrategies
-from .strategy.dict import DictStrategies
-from .strategy.set import SetStrategies
-from .strategy.type_conflict import TypeConflictStrategies
-from .strategy.fallback import FallbackStrategies
+from __future__ import annotations
+from typing import Dict, List, Type, Tuple, Any, Union, Sequence, Callable
-class Merger(object):
+from . import strategy as s
+
+
+class Merger:
"""
+ Merges objects based on provided strategies
+
:param type_strategies, List[Tuple]: a list of (Type, Strategy) pairs
that should be used against incoming types. For example: (dict, "override").
"""
- PROVIDED_TYPE_STRATEGIES = {
- list: ListStrategies,
- dict: DictStrategies,
- set: SetStrategies,
+ PROVIDED_TYPE_STRATEGIES: Dict[Type, Type[s.StrategyList]] = {
+ list: s.ListStrategies,
+ dict: s.DictStrategies,
+ set: s.SetStrategies,
}
- def __init__(self, type_strategies, fallback_strategies, type_conflict_strategies):
- self._fallback_strategy = FallbackStrategies(fallback_strategies)
+ def __init__(
+ self,
+ type_strategies: Sequence[
+ Tuple[Type, Union[s.StrategyCallable, s.StrategyListInitable]]
+ ],
+ fallback_strategies: s.StrategyListInitable,
+ type_conflict_strategies: s.StrategyListInitable,
+ ) -> None:
+ self._fallback_strategy = s.FallbackStrategies(fallback_strategies)
+ self._type_conflict_strategy = s.TypeConflictStrategies(
+ type_conflict_strategies
+ )
- expanded_type_strategies = []
+ self._type_strategies: List[Tuple[Type, s.StrategyCallable]] = []
for typ, strategy in type_strategies:
if typ in self.PROVIDED_TYPE_STRATEGIES:
- strategy = self.PROVIDED_TYPE_STRATEGIES[typ](strategy)
- expanded_type_strategies.append((typ, strategy))
- self._type_strategies = expanded_type_strategies
+ # Customise a StrategyList instance for this type
+ self._type_strategies.append(
+ (typ, self.PROVIDED_TYPE_STRATEGIES[typ](strategy))
+ )
+ elif callable(strategy):
+ self._type_strategies.append((typ, strategy))
+ else:
+ raise ValueError(f"Cannot handle ({typ}, {strategy})")
+ return
- self._type_conflict_strategy = TypeConflictStrategies(type_conflict_strategies)
-
- def merge(self, base, nxt):
+ def merge(self, base: Any, nxt: Any) -> Any:
return self.value_strategy([], base, nxt)
- def type_conflict_strategy(self, *args):
- return self._type_conflict_strategy(self, *args)
+ def type_conflict_strategy(self, path: List, base: Any, nxt: Any) -> Any:
+ return self._type_conflict_strategy(self, path, base, nxt)
- def value_strategy(self, path, base, nxt):
+ def value_strategy(self, path: List, base: Any, nxt: Any) -> Any:
+ # Check for strategy based on type of base, next
for typ, strategy in self._type_strategies:
if isinstance(base, typ) and isinstance(nxt, typ):
+ # We have a strategy for this type
return strategy(self, path, base, nxt)
- if not (isinstance(base, type(nxt)) or isinstance(nxt, type(base))):
- return self.type_conflict_strategy(path, base, nxt)
- return self._fallback_strategy(self, path, base, nxt)
+
+ if isinstance(base, type(nxt)) or isinstance(nxt, type(base)):
+ # no known strategy but base, next are similar types
+ return self._fallback_strategy(self, path, base, nxt)
+
+ # No known strategy and base, next are different types.
+ return self.type_conflict_strategy(path, base, nxt)
diff --git a/deepmerge/py.typed b/deepmerge/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/deepmerge/strategy/__init__.py b/deepmerge/strategy/__init__.py
index e69de29..d62ea7c 100644
--- a/deepmerge/strategy/__init__.py
+++ b/deepmerge/strategy/__init__.py
@@ -0,0 +1,6 @@
+from .core import StrategyList, StrategyCallable, StrategyListInitable
+from .dict import DictStrategies
+from .fallback import FallbackStrategies
+from .list import ListStrategies
+from .set import SetStrategies
+from .type_conflict import TypeConflictStrategies
diff --git a/deepmerge/strategy/core.py b/deepmerge/strategy/core.py
index 91fcfbb..2b2f3cc 100644
--- a/deepmerge/strategy/core.py
+++ b/deepmerge/strategy/core.py
@@ -1,20 +1,38 @@
+from __future__ import annotations
+
+import sys
+from typing import TYPE_CHECKING, List, Callable, Any, Union
+
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+else:
+ from typing_extentions import TypeAlias
+
+import deepmerge.merger
from ..exception import StrategyNotFound, InvalidMerge
-from ..compat import string_type
STRATEGY_END = object()
+StrategyCallable: TypeAlias = "Callable[[deepmerge.merger.Merger, List, Any, Any], Any]"
+StrategyListInitable: TypeAlias = (
+ "Union[str, StrategyCallable, List[Union[str, StrategyCallable]]]"
+)
-class StrategyList(object):
- NAME = None
+class StrategyList:
+ NAME: str
- def __init__(self, strategy_list):
+ def __init__(self, strategy_list: StrategyListInitable) -> None:
if not isinstance(strategy_list, list):
strategy_list = [strategy_list]
- self._strategies = [self._expand_strategy(s) for s in strategy_list]
+ self._strategies: List[StrategyCallable] = [
+ self._expand_strategy(s) for s in strategy_list
+ ]
@classmethod
- def _expand_strategy(cls, strategy):
+ def _expand_strategy(
+ cls, strategy: Union[str, StrategyCallable]
+ ) -> StrategyCallable:
"""
:param strategy: string or function
@@ -23,16 +41,18 @@ def _expand_strategy(cls, strategy):
Otherwise, return the value, implicitly assuming it's a function.
"""
- if isinstance(strategy, string_type):
- method_name = "strategy_{0}".format(strategy)
- if not hasattr(cls, method_name):
- raise StrategyNotFound(strategy)
- return getattr(cls, method_name)
+ if isinstance(strategy, str):
+ method_name = f"strategy_{strategy}"
+ if hasattr(cls, method_name):
+ return getattr(cls, method_name)
+ raise StrategyNotFound(strategy)
return strategy
- def __call__(self, *args, **kwargs):
+ def __call__(
+ self, config: deepmerge.merger.Merger, path: List, base: Any, nxt: Any
+ ) -> Any:
for s in self._strategies:
- ret_val = s(*args, **kwargs)
+ ret_val = s(config, path, base, nxt)
if ret_val is not STRATEGY_END:
return ret_val
- raise InvalidMerge(self.NAME, args, kwargs)
+ raise InvalidMerge(self.NAME, config, path, base, nxt)
diff --git a/deepmerge/strategy/dict.py b/deepmerge/strategy/dict.py
index 8f09eb9..b883558 100644
--- a/deepmerge/strategy/dict.py
+++ b/deepmerge/strategy/dict.py
@@ -1,3 +1,8 @@
+from __future__ import annotations
+
+from typing import Dict, List
+
+import deepmerge.merger
from .core import StrategyList
@@ -10,7 +15,9 @@ class DictStrategies(StrategyList):
NAME = "dict"
@staticmethod
- def strategy_merge(config, path, base, nxt):
+ def strategy_merge(
+ config: deepmerge.merger.Merger, path: List, base: Dict, nxt: Dict
+ ) -> Dict:
"""
for keys that do not exists,
use them directly. if the key exists
@@ -24,7 +31,9 @@ def strategy_merge(config, path, base, nxt):
return base
@staticmethod
- def strategy_override(config, path, base, nxt):
+ def strategy_override(
+ config: deepmerge.merger.Merger, path: List, base: Dict, nxt: Dict
+ ) -> Dict:
"""
move all keys in nxt into base, overriding
conflicts.
diff --git a/deepmerge/strategy/fallback.py b/deepmerge/strategy/fallback.py
index 714e8f9..c024594 100644
--- a/deepmerge/strategy/fallback.py
+++ b/deepmerge/strategy/fallback.py
@@ -1,6 +1,14 @@
+from __future__ import annotations
+
+from typing import List, Any, TypeVar
+
+import deepmerge.merger
from .core import StrategyList
+T = TypeVar("T")
+
+
class FallbackStrategies(StrategyList):
"""
The StrategyList containing fallback strategies.
@@ -9,11 +17,15 @@ class FallbackStrategies(StrategyList):
NAME = "fallback"
@staticmethod
- def strategy_override(config, path, base, nxt):
+ def strategy_override(
+ config: deepmerge.merger.Merger, path: List, base: Any, nxt: T
+ ) -> T:
"""use nxt, and ignore base."""
return nxt
@staticmethod
- def strategy_use_existing(config, path, base, nxt):
+ def strategy_use_existing(
+ config: deepmerge.merger.Merger, path: List, base: T, nxt: Any
+ ) -> T:
"""use base, and ignore next."""
return base
diff --git a/deepmerge/strategy/list.py b/deepmerge/strategy/list.py
index 2e42519..704d433 100644
--- a/deepmerge/strategy/list.py
+++ b/deepmerge/strategy/list.py
@@ -1,3 +1,8 @@
+from __future__ import annotations
+
+from typing import List
+
+import deepmerge.merger
from .core import StrategyList
from ..extended_set import ExtendedSet
@@ -10,22 +15,30 @@ class ListStrategies(StrategyList):
NAME = "list"
@staticmethod
- def strategy_override(config, path, base, nxt):
+ def strategy_override(
+ config: deepmerge.merger.Merger, path: List, base: List, nxt: List
+ ) -> List:
"""use the list nxt."""
return nxt
@staticmethod
- def strategy_prepend(config, path, base, nxt):
+ def strategy_prepend(
+ config: deepmerge.merger.Merger, path: List, base: List, nxt: List
+ ) -> List:
"""prepend nxt to base."""
return nxt + base
@staticmethod
- def strategy_append(config, path, base, nxt):
+ def strategy_append(
+ config: deepmerge.merger.Merger, path: List, base: List, nxt: List
+ ) -> List:
"""append nxt to base."""
return base + nxt
@staticmethod
- def strategy_append_unique(config, path, base, nxt):
+ def strategy_append_unique(
+ config: deepmerge.merger.Merger, path: List, base: List, nxt: List
+ ) -> List:
"""append items without duplicates in nxt to base."""
base_as_set = ExtendedSet(base)
return base + [n for n in nxt if n not in base_as_set]
diff --git a/deepmerge/strategy/set.py b/deepmerge/strategy/set.py
index 55b26e3..40cf4b0 100644
--- a/deepmerge/strategy/set.py
+++ b/deepmerge/strategy/set.py
@@ -1,3 +1,8 @@
+from __future__ import annotations
+
+from typing import List, Set
+
+import deepmerge.merger
from .core import StrategyList
@@ -9,21 +14,27 @@ class SetStrategies(StrategyList):
NAME = "set"
@staticmethod
- def strategy_union(config, path, base, nxt):
+ def strategy_union(
+ config: deepmerge.merger.Merger, path: List, base: Set, nxt: Set
+ ) -> Set:
"""
use all values in either base or nxt.
"""
return base | nxt
@staticmethod
- def strategy_intersect(config, path, base, nxt):
+ def strategy_intersect(
+ config: deepmerge.merger.Merger, path: List, base: Set, nxt: Set
+ ) -> Set:
"""
use all values in both base and nxt.
"""
return base & nxt
@staticmethod
- def strategy_override(config, path, base, nxt):
+ def strategy_override(
+ config: deepmerge.merger.Merger, path: List, base: Set, nxt: Set
+ ) -> Set:
"""
use the set nxt.
"""
diff --git a/deepmerge/strategy/type_conflict.py b/deepmerge/strategy/type_conflict.py
index ebbc77a..6d32bfc 100644
--- a/deepmerge/strategy/type_conflict.py
+++ b/deepmerge/strategy/type_conflict.py
@@ -1,5 +1,13 @@
+from __future__ import annotations
+
+from typing import List, Union, Any, TypeVar
+
+import deepmerge.merger
from .core import StrategyList
+T1 = TypeVar("T1")
+T2 = TypeVar("T2")
+
class TypeConflictStrategies(StrategyList):
"""contains the strategies provided for type conflicts."""
@@ -7,16 +15,22 @@ class TypeConflictStrategies(StrategyList):
NAME = "type conflict"
@staticmethod
- def strategy_override(config, path, base, nxt):
+ def strategy_override(
+ config: deepmerge.merger.Merger, path: List, base: Any, nxt: T1
+ ) -> T1:
"""overrides the new object over the old object"""
return nxt
@staticmethod
- def strategy_use_existing(config, path, base, nxt):
+ def strategy_use_existing(
+ config: deepmerge.merger.Merger, path: List, base: T1, nxt: Any
+ ) -> T1:
"""uses the old object instead of the new object"""
return base
@staticmethod
- def strategy_override_if_not_empty(config, path, base, nxt):
+ def strategy_override_if_not_empty(
+ config: deepmerge.merger.Merger, path: List, base: T1, nxt: T2
+ ) -> Union[T1, T2]:
"""overrides the new object over the old object only if the new object is not empty or null"""
return nxt if nxt else base
diff --git a/deepmerge/tests/strategy/test_core.py b/deepmerge/tests/strategy/test_core.py
index afa76d7..b96922a 100644
--- a/deepmerge/tests/strategy/test_core.py
+++ b/deepmerge/tests/strategy/test_core.py
@@ -12,16 +12,6 @@ def always_return_custom(config, path, base, nxt):
return "custom"
-def test_single_value_allowed():
- """ """
-
- def strat(name):
- return name
-
- sl = StrategyList(strat)
- assert sl("foo") == "foo"
-
-
def test_first_working_strategy_is_used():
"""
In the case where the StrategyList has multiple values,
diff --git a/deepmerge/tests/strategy/test_type_conflict.py b/deepmerge/tests/strategy/test_type_conflict.py
index a366351..88487cc 100644
--- a/deepmerge/tests/strategy/test_type_conflict.py
+++ b/deepmerge/tests/strategy/test_type_conflict.py
@@ -1,6 +1,8 @@
+from typing import Dict
+
from deepmerge.strategy.type_conflict import TypeConflictStrategies
-EMPTY_DICT = {}
+EMPTY_DICT: Dict = {}
CONTENT_AS_LIST = [{"key": "val"}]
diff --git a/deepmerge/tests/test_full.py b/deepmerge/tests/test_full.py
index 799db19..7a1fe9f 100644
--- a/deepmerge/tests/test_full.py
+++ b/deepmerge/tests/test_full.py
@@ -31,8 +31,10 @@ def test_merge_or_raise_raises_exception():
merge_or_raise.merge(base, nxt)
exc = exc_info.value
assert exc.strategy_list_name == "type conflict"
- assert exc.merge_args == (merge_or_raise, ["foo"], 0, "a string!")
- assert exc.merge_kwargs == {}
+ assert exc.config == merge_or_raise
+ assert exc.path == ["foo"]
+ assert exc.base == 0
+ assert exc.nxt == "a string!"
@pytest.mark.parametrize(
diff --git a/pyproject.toml b/pyproject.toml
index 52553c3..4467e31 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,8 +1,65 @@
[build-system]
-# setuptools and setuptools_scm version are explicitly set
-# to allow support for python2
-requires = ["setuptools>=44", "wheel", "setuptools_scm>=5"]
+requires = ["setuptools>=69", "setuptools_scm>=8"]
build-backend = "setuptools.build_meta"
+[project]
+name = "deepmerge"
+description = "A toolset for deeply merging Python dictionaries."
+authors = [
+ {name = "Yusuke Tsutsumi"},
+]
+
+# Dependency Information
+requires-python = ">=3.7"
+dependencies = [
+ "typing_extensions;python_version<='3.9'",
+]
+
+# Extra Information
+readme = "README.rst"
+license = {text = "MIT Licence"}
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Typing :: Typed",
+]
+
+dynamic = ["version"]
+
+[project.urls]
+Homepage = "http://deepmerge.readthedocs.io/en/latest/"
+GitHub = "https://github.com/toumorokoshi/deepmerge"
+
+[project.optional-dependencies]
+dev = [
+ ## Formatting / Linting
+ "validate-pyproject[all]",
+ "pyupgrade",
+ "black",
+ "pylint",
+ "mypy",
+ ## Testing
+ "pytest",
+]
+
+[tool.setuptools.packages.find]
+include = ["deepmerge*"]
+exclude = ["tests*", "*.tests*"]
+
+[tool.setuptools.package-data]
+deepmerge = ["py.typed"]
+
[tool.setuptools_scm]
-write_to = "deepmerge/_version.py"
\ No newline at end of file
+write_to = "deepmerge/_version.py"
+
+[tool.black]
+line-length = 88
+target-version = ["py37", "py38", "py39", "py310", "py311", "py312"]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 00c5503..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,19 +0,0 @@
-[metadata]
-name = deepmerge
-description = a toolset to deeply merge python dictionaries.
-long_description = file: README.rst
-url = http://deepmerge.readthedocs.io/en/latest/
-classifiers =
- Development Status :: 5 - Production/Stable
- License :: OSI Approved :: MIT License
- Programming Language :: Python :: 3
-python_requires = >=3.*
-
-[options]
-zip_safe = true
-packages = find:
-
-[options.packages.find]
-exclude =
- tests*
- *.tests*
\ No newline at end of file