Skip to content

Commit

Permalink
Fixed data merge organization
Browse files Browse the repository at this point in the history
- Moved data merge constants and functions from composition.py to data_merge.py
  • Loading branch information
coordt committed Sep 23, 2023
1 parent ee03691 commit 4bba0f4
Show file tree
Hide file tree
Showing 14 changed files with 73 additions and 118 deletions.
8 changes: 5 additions & 3 deletions cookie_composer/cc_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from cookie_composer.data_merge import Context


def jsonify_context(value: Any) -> dict:
def jsonify_context(value: Any) -> MutableMapping:
"""Convert a ``Context`` to a dict."""
if isinstance(value, Context):
return value.flatten()
Expand Down Expand Up @@ -123,7 +123,7 @@ def prompt_for_config(
return context


def _render_dicts(context: dict, env: Environment, no_input: bool, prompts: dict) -> None:
def _render_dicts(context: MutableMapping, env: Environment, no_input: bool, prompts: dict) -> None:
"""
Render dictionaries.
Expand Down Expand Up @@ -161,7 +161,9 @@ def _render_dicts(context: dict, env: Environment, no_input: bool, prompts: dict
raise UndefinedVariableInTemplate(msg, err, context) from err


def _render_simple(context: dict, context_prompts: dict, env: Environment, no_input: bool, prompts: dict) -> None:
def _render_simple(
context: MutableMapping, context_prompts: dict, env: Environment, no_input: bool, prompts: dict
) -> None:
"""
Render simple variables, raw variables, and choices.
Expand Down
52 changes: 1 addition & 51 deletions cookie_composer/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,12 @@

from pydantic import BaseModel, DirectoryPath, Field, root_validator

from cookie_composer.data_merge import comprehensive_merge
from cookie_composer.data_merge import DO_NOT_MERGE, comprehensive_merge
from cookie_composer.exceptions import GitError, MissingCompositionFileError
from cookie_composer.matching import rel_fnmatch

logger = logging.getLogger(__name__)


# Strategies merging files and data.

DO_NOT_MERGE = "do-not-merge"
"""Do not merge the data, use the file path to determine what to do."""

NESTED_OVERWRITE = "nested-overwrite"
"""Merge deeply nested structures and overwrite at the lowest level; A deep ``dict.update()``."""

OVERWRITE = "overwrite"
"""Overwrite at the top level like ``dict.update()``."""

COMPREHENSIVE = "comprehensive"
"""Comprehensively merge the two data structures.
- Scalars are overwritten by the new values
- lists are merged and de-duplicated
- dicts are recursively merged
"""


class LayerConfig(BaseModel):
"""Configuration for a layer of a composition."""

Expand Down Expand Up @@ -301,35 +280,6 @@ def write_rendered_composition(composition: RenderedComposition) -> None:
write_composition(layers, composition_file)


def get_merge_strategy(path: Path, merge_strategies: Dict[str, str]) -> str:
"""
Return the merge strategy of the path based on the layer configured rules.
Files that are not mergable return :attr:`~cookie_composer.composition.DO_NOT_MERGE`
Args:
path: The file path to evaluate.
merge_strategies: The glob pattern->strategy mapping
Returns:
The appropriate merge strategy.
"""
from cookie_composer.merge_files import MERGE_FUNCTIONS

strategy = DO_NOT_MERGE # The default

if path.suffix not in MERGE_FUNCTIONS:
return DO_NOT_MERGE

for pattern, strat in merge_strategies.items():
if rel_fnmatch(str(path), pattern):
logger.debug(f"{path} matches merge strategy pattern {pattern} for {strat}")
strategy = strat
break

return strategy


def get_composition_from_path_or_url(
path_or_url: str,
checkout: Optional[str] = None,
Expand Down
58 changes: 56 additions & 2 deletions cookie_composer/data_merge.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"""Tools for merging data."""
import copy
import logging
from collections import ChainMap, OrderedDict
from functools import reduce
from typing import Any, Iterable, MutableMapping
from pathlib import Path
from typing import Any, Dict, Iterable, MutableMapping

from immutabledict import immutabledict

from cookie_composer.matching import rel_fnmatch

logger = logging.getLogger(__name__)


def deep_merge(*dicts: dict) -> dict:
"""
Expand Down Expand Up @@ -107,7 +113,7 @@ def is_empty(self) -> bool:
"""The context has only one mapping and it is empty."""
return len(self.maps) == 1 and len(self.maps[0]) == 0

def flatten(self) -> dict:
def flatten(self) -> MutableMapping:
"""Comprehensively merge all the maps into a single mapping."""
return reduce(comprehensive_merge, self.maps, {})

Expand All @@ -125,3 +131,51 @@ def freeze_data(obj: Any) -> Any:
elif isinstance(obj, (set, frozenset)):
return frozenset(freeze_data(i) for i in obj)
raise ValueError(obj)


# Strategies merging files and data.
DO_NOT_MERGE = "do-not-merge"
"""Do not merge the data, use the file path to determine what to do."""

NESTED_OVERWRITE = "nested-overwrite"
"""Merge deeply nested structures and overwrite at the lowest level; A deep ``dict.update()``."""

OVERWRITE = "overwrite"
"""Overwrite at the top level like ``dict.update()``."""

COMPREHENSIVE = "comprehensive"
"""Comprehensively merge the two data structures.
- Scalars are overwritten by the new values
- lists are merged and de-duplicated
- dicts are recursively merged
"""


def get_merge_strategy(path: Path, merge_strategies: Dict[str, str]) -> str:
"""
Return the merge strategy of the path based on the layer configured rules.
Files that are not mergable return :attr:`~cookie_composer.composition.DO_NOT_MERGE`
Args:
path: The file path to evaluate.
merge_strategies: The glob pattern->strategy mapping
Returns:
The appropriate merge strategy.
"""
from cookie_composer.merge_files import MERGE_FUNCTIONS

strategy = DO_NOT_MERGE # The default

if path.suffix not in MERGE_FUNCTIONS:
return DO_NOT_MERGE

for pattern, strat in merge_strategies.items():
if rel_fnmatch(str(path), pattern):
logger.debug(f"{path} matches merge strategy pattern {pattern} for {strat}")
strategy = strat
break

return strategy
4 changes: 1 addition & 3 deletions cookie_composer/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@

from cookie_composer.cc_overrides import prompt_for_config
from cookie_composer.composition import (
DO_NOT_MERGE,
LayerConfig,
RenderedLayer,
get_merge_strategy,
)
from cookie_composer.data_merge import Context
from cookie_composer.data_merge import DO_NOT_MERGE, Context, get_merge_strategy
from cookie_composer.matching import matches_any_glob
from cookie_composer.merge_files import MERGE_FUNCTIONS

Expand Down
7 changes: 1 addition & 6 deletions cookie_composer/merge_files/ini_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
from typing import Dict

from cookie_composer import data_merge
from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import COMPREHENSIVE, DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE
from cookie_composer.exceptions import MergeError


Expand Down
7 changes: 1 addition & 6 deletions cookie_composer/merge_files/json_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
import orjson

from cookie_composer import data_merge
from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import COMPREHENSIVE, DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE
from cookie_composer.exceptions import MergeError


Expand Down
7 changes: 1 addition & 6 deletions cookie_composer/merge_files/toml_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
import toml

from cookie_composer import data_merge
from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import COMPREHENSIVE, DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE
from cookie_composer.exceptions import MergeError


Expand Down
7 changes: 1 addition & 6 deletions cookie_composer/merge_files/yaml_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
from immutabledict import immutabledict

from cookie_composer import data_merge
from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import COMPREHENSIVE, DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE
from cookie_composer.exceptions import MergeError


Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ classifiers = [
"Topic :: Software Development :: Code Generators",
]
dependencies = [
"backports.shutil-copytree",
"click-log",
"cookiecutter>=2.2.0",
"fsspec",
Expand Down
12 changes: 2 additions & 10 deletions tests/test_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,10 @@
from cookiecutter.config import get_user_config

from cookie_composer import layers
from cookie_composer.composition import (
DO_NOT_MERGE,
OVERWRITE,
LayerConfig,
RenderedLayer,
)
from cookie_composer.data_merge import Context, comprehensive_merge
from cookie_composer.composition import LayerConfig, RenderedLayer
from cookie_composer.data_merge import Context, comprehensive_merge, DO_NOT_MERGE, OVERWRITE
from cookie_composer.git_commands import get_latest_template_commit

if "dirs_exist_ok" not in signature(copytree).parameters:
from backports.shutil_copytree import copytree


def test_render_layer(fixtures_path, tmp_path):
"""Test rendering a layer."""
Expand Down
7 changes: 1 addition & 6 deletions tests/test_merge_files_ini_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@

import pytest

from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE, COMPREHENSIVE
from cookie_composer.exceptions import MergeError
from cookie_composer.merge_files import ini_file

Expand Down
7 changes: 1 addition & 6 deletions tests/test_merge_files_json_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@

import pytest

from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE, COMPREHENSIVE
from cookie_composer.exceptions import MergeError
from cookie_composer.merge_files import json_file

Expand Down
7 changes: 1 addition & 6 deletions tests/test_merge_files_toml_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
import pytest
import toml

from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE, COMPREHENSIVE
from cookie_composer.exceptions import MergeError
from cookie_composer.merge_files import toml_file

Expand Down
7 changes: 1 addition & 6 deletions tests/test_merge_files_yaml_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
import pytest
from ruamel.yaml import YAML

from cookie_composer.composition import (
COMPREHENSIVE,
DO_NOT_MERGE,
NESTED_OVERWRITE,
OVERWRITE,
)
from cookie_composer.data_merge import DO_NOT_MERGE, NESTED_OVERWRITE, OVERWRITE, COMPREHENSIVE
from cookie_composer.exceptions import MergeError
from cookie_composer.merge_files import yaml_file

Expand Down

0 comments on commit 4bba0f4

Please sign in to comment.