diff --git a/Makefile b/Makefile index 806ba7f..f8879a2 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ build: .venv/deps rm -rf ./dist/ .venv/bin/python -m build +format: .venv/deps + .venv/bin/black deepmerge + # only works with python 3+ lint: .venv/deps .venv/bin/validate-pyproject pyproject.toml @@ -18,4 +21,15 @@ lint: .venv/deps test: .venv/deps .venv/bin/pytest deepmerge +docs: .venv/deps + $(MAKE) -C docs html + +docs-serve: docs + .venv/bin/python -m http.server --directory docs/_build/html + ready-pr: test lint + +clean: + rm -rf .venv + rm -rf build + \ No newline at end of file diff --git a/deepmerge/tests/test_full.py b/deepmerge/tests/test_full.py index bc6bdee..c9246ff 100644 --- a/deepmerge/tests/test_full.py +++ b/deepmerge/tests/test_full.py @@ -58,3 +58,40 @@ def test_subtypes(): result = always_merger.merge(base, next) assert dict(result) == {"foo": "value", "bar": "value2", "baz": ["a", "b"]} + + +@pytest.mark.parametrize( + "initializer,expectation", + [ + (None, {"foo": "value", "bar": "value3", "baz": ["a", "b", "c"]}), + ({}, {"foo": "value", "bar": "value3", "baz": ["a", "b", "c"]}), + ( + {"init": "value"}, + {"foo": "value", "bar": "value3", "baz": ["a", "b", "c"], "init": "value"}, + ), + ], + ids=["no initilaizer", "empty initializer", "set initializer"], +) +def test_reduce(initializer, expectation): + # Given + from functools import reduce + + base = {"foo": "value", "baz": ["a"]} + next = {"bar": "value2", "baz": ["b"]} + more = {"bar": "value3", "baz": ["c"]} + + list_to_merge = [base, next, more] + + # When + if initializer is None: + result = reduce(always_merger.merge, list_to_merge) + else: + result = reduce(always_merger.merge, list_to_merge, initializer) + + # Then + assert result == expectation + + if initializer is None: + assert result is base + else: + assert result is not base diff --git a/docs/api.rst b/docs/api.rst index 410df60..5b2f508 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,9 +2,35 @@ API Reference ============= +Merger +====== + .. autoclass:: deepmerge.merger.Merger :members: +Strategies +========== + +.. automodule:: deepmerge.strategy.core + :members: + +.. automodule:: deepmerge.strategy.dict + :members: + +.. automodule:: deepmerge.strategy.list + :members: + +.. automodule:: deepmerge.strategy.set + :members: + +.. automodule:: deepmerge.strategy.fallback + :members: + +.. automodule:: deepmerge.strategy.type_conflict + :members: + +Exceptions +========== .. automodule:: deepmerge.exception :members: diff --git a/docs/conf.py b/docs/conf.py index df0dc4d..a491ea5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -70,7 +70,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -129,7 +129,7 @@ import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # DEPRECATED # html_theme = 'alabaster' @@ -165,7 +165,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +# html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied diff --git a/docs/guide.rst b/docs/guide.rst index 8145e1c..07384ac 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -30,6 +30,8 @@ Once a merger is constructed, it then has a merge() method that can be called: "baz": ["a", "b"] } +.. _merges_are_destructive: + Merges are Destructive ====================== @@ -45,6 +47,72 @@ This is intentional, as an implicit copy would result in a significant performan assert id(result) != id(base) +`functools.reduce` Usage +======================== + +If you have an iterable collection of data structures to merge, you can use `functools.reduce` to merge them all together. +Beware that there is some nuanced behaviour when using the initialiser with `functools.reduce` +which relates to the :ref:`merges_are_destructive` section above. + + +**Example 1**: Not setting an initialiser + +.. code-block:: python + + from deepmerge import always_merger + from functools import reduce + + base = {"foo": "value", "baz": ["a"]} + next = {"bar": "value2", "baz": ["b"]} + more = {"bar": "value3", "baz": ["c"]} + + list_to_merge = [base, next, more] + + result = reduce(always_merger.merge, list_to_merge) + + assert result == { + "foo": "value", + "bar": "value3", + "baz": ["a", "b", "c"] + } + assert result == base + +**Example 2**: Setting an empty initialiser + +Where as the following will not impact `base` because we initialise a new empty `dict`: + +.. code-block:: python + + result = reduce(always_merger.merge, list_to_merge, {}) + + assert result == { + "foo": "value", + "bar": "value3", + "baz": ["a", "b", "c"] + } + assert result != base + +Which can lead to some fun and easy configuration loading implementations like: + +.. code-block:: python + + import pathlib + import yaml #PyYAML + from functools import reduce + from deepmerge import always_merger + + # Recursively find yaml files, parse and then merge them through pythonic map-reduce idioms + my_configuration = reduce( + always_merger.merge, + map( + lambda f: yaml.safe_load(f.read_text()), + pathlib.Path.cwd().glob("**/*.yml") + ), + {} + ) + + + Authoring your own Mergers ========================== diff --git a/pyproject.toml b/pyproject.toml index e75ee0d..5f5c973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,9 @@ dev = [ ## Build / Release "build", "twine", + ## Documentation + "sphinx", + "sphinx-rtd-theme" ] [tool.setuptools.packages.find]