Skip to content

Commit

Permalink
✨ Add reporting callback.
Browse files Browse the repository at this point in the history
Closes #2
  • Loading branch information
rafalkrupinski committed Feb 23, 2024
1 parent a2223ac commit 919e2e8
Show file tree
Hide file tree
Showing 34 changed files with 171 additions and 111 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

### Added

- Add parameter type to the TypeError when it's not an Iterable.
- Add parameter type to the TypeError thrown when the input is not Iterable.
- Accept a report callback, called for every written file.

### Fixed

Expand Down
10 changes: 8 additions & 2 deletions src/rybak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@

from itertools import chain
from pathlib import Path, PurePath
from typing import Iterable, Union
from typing import Any, Iterable, Union

from ._types import RenderError, TemplateData
from ._types import RenderError, ReportCallbackFn, TemplateData
from .adapter import RendererAdapter
from .tree_renderer import RenderContext, TreeRenderer


def _noop_report_cb(*_: Any) -> None:
pass


def render(
adapter: RendererAdapter,
data: TemplateData,
Expand All @@ -21,6 +25,7 @@ def render(
exclude: Union[Iterable[Path], Iterable[str]] = ('__pycache__',),
exclude_extend: Union[Iterable[Path], Iterable[str]] = (),
remove_suffixes: Iterable[str] = (),
report_cb: ReportCallbackFn = _noop_report_cb,
) -> None:
"""Render a directory-tree from a template and a data dictionary
Expand All @@ -38,6 +43,7 @@ def render(
target_root=target_root,
exclude=exclude_paths,
remove_suffixes=remove_suffixes,
report_cb=report_cb,
),
PurePath(),
Path(),
Expand Down
3 changes: 3 additions & 0 deletions src/rybak/_types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pathlib
from typing import Any, Callable, Iterable, Mapping, TypeVar

from .pycompat import TypeAlias
Expand All @@ -8,6 +9,8 @@
LoopOverFn: TypeAlias = Callable[[Iterable[Item]], Item]
RenderFn: TypeAlias = Callable[[str, str, TemplateData], None]

ReportCallbackFn: TypeAlias = Callable[[pathlib.PurePath, pathlib.Path], None]


class RenderError(Exception):
pass
17 changes: 11 additions & 6 deletions src/rybak/tree_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path, PurePath
from typing import Iterable, NoReturn

from ._types import LoopOverFn, RenderFn, TemplateData
from ._types import LoopOverFn, RenderFn, ReportCallbackFn, TemplateData
from .adapter import RendererAdapter
from .pycompat import Traversable

Expand Down Expand Up @@ -36,6 +36,7 @@ class RenderContext:
adapter: RendererAdapter
exclude: Iterable[PurePath]
remove_suffixes: Iterable[str]
report_cb: ReportCallbackFn


class TreeRenderer:
Expand Down Expand Up @@ -110,12 +111,16 @@ def _render_file_name(self, template: str, data: TemplateData, loop_over_: LoopO
return target_name

def _render_file(self, template_name: str, target_name: str, data: TemplateData) -> None:
logger.debug('Render to file %s', self._target_path / target_name)
target_path = self._full_target_path / target_name
target_path.parent.mkdir(parents=True, exist_ok=True)
source = self._template_path / template_name
target = self._target_path / target_name
target_full = self._context.target_root / target
target_full.parent.mkdir(parents=True, exist_ok=True)

self._context.report_cb(source, target)

self._context.adapter.render_file(
(self._template_path / template_name).as_posix(),
target_path,
source.as_posix(),
target_full,
data,
)

Expand Down
125 changes: 125 additions & 0 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import logging
import sys
from itertools import product
from pathlib import Path
from typing import Any, Callable, Iterable, Mapping, NamedTuple, Optional

import jinja2
import pytest
from rybak import RenderError, render
from rybak.adapter import RendererAdapter
from rybak.jinja import JinjaAdapter
from rybak.mako import MakoAdapter

from tests.compare import cmp_dirs

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


class E2eTestData(NamedTuple):
test_name: str
data: Mapping[str, Any]
error: Optional[bool] = False


e2e_test_data: Iterable[E2eTestData] = [
E2eTestData(
'simple',
dict(
tmpl_dir='target_dir',
tmpl_file1='file1.txt',
tmpl_file2='file2.txt',
tmpl_file3='subdir/file3.txt',
content1='foo',
content2='bar',
content3='baz',
empty_directory_name='',
empty_file_name='',
),
),
E2eTestData(
'loop',
dict(
animals={
'cat': 'meows',
'dog': 'barks',
'': 'is silent',
}
),
),
E2eTestData(
'loop_nested',
dict(
animals=dict(
cats=dict(
Loki='black, white, red',
Judo='black, white',
),
dogs=dict(
Pluto='golden',
Goofy='black',
),
)
),
),
E2eTestData(
'loop_nested',
{},
error=True,
),
E2eTestData(
'missing_file',
{},
error=True,
),
]

adapters = {
'jinja': lambda template_root: JinjaAdapter(loader=jinja2.FileSystemLoader(template_root)),
'mako': MakoAdapter,
}

exclusions = {
'jinja': ['{{tmpl_dir}}/excluded_file.txt'],
'mako': ['${tmpl_dir}/excluded_file.txt'],
}

adapter_test_data = [
(*adapter, *param_set, exclusions[adapter[0]]) for adapter, param_set in product(adapters.items(), e2e_test_data)
]


@pytest.mark.parametrize('adapter_name,adapter,test_name,data,error,exclude', adapter_test_data)
def test_render(
adapter_name: str,
adapter: Callable[[Path], RendererAdapter],
test_name: str,
data: Mapping,
error: bool,
exclude: Iterable[str],
tmp_path: Path,
) -> None:
if adapter_name == 'mako' and sys.platform == 'win32':
pytest.skip('Mako has problem with line endings on windows')

root = Path(__file__).parent / 'test_e2e'
target_path = tmp_path / f'{adapter_name}_{test_name}'
target_path.mkdir()

def fn():
render(
adapter(root / 'templates' / adapter_name / test_name),
data,
target_path,
exclude_extend=exclude,
remove_suffixes=['.jinja', '.mako'],
report_cb=lambda _, target: logger.debug('Render to file %s', target),
)

if error:
with pytest.raises(RenderError):
fn()
else:
fn()
cmp_dirs(root / 'output' / test_name, target_path)
File renamed without changes.
File renamed without changes.
File renamed without changes.
124 changes: 22 additions & 102 deletions tests/test_render.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import sys
from itertools import product
from pathlib import Path
from typing import Any, Callable, Iterable, Mapping, NamedTuple, Optional
import pathlib

import jinja2
import pytest
from rybak import RenderError, render
from rybak.adapter import RendererAdapter
from rybak.jinja import JinjaAdapter
from rybak.mako import MakoAdapter
import rybak
import rybak.jinja

from tests.compare import cmp_dirs

def test_callback(tmp_path: pathlib.Path):
logs = set()

class E2eTestData(NamedTuple):
test_name: str
data: Mapping[str, Any]
error: Optional[bool] = False
def report_fb(source: pathlib.PurePath, target: pathlib.Path) -> None:
logs.add((source, target))

template_root = pathlib.Path(__file__).parent / 'test_e2e' / 'templates' / 'jinja' / 'simple'

e2e_test_data: Iterable[E2eTestData] = [
E2eTestData(
'simple',
rybak.render(
rybak.jinja.JinjaAdapter(loader=jinja2.FileSystemLoader(template_root)),
dict(
tmpl_dir='target_dir',
tmpl_file1='file1.txt',
Expand All @@ -33,88 +26,15 @@ class E2eTestData(NamedTuple):
empty_directory_name='',
empty_file_name='',
),
),
E2eTestData(
'loop',
dict(
animals={
'cat': 'meows',
'dog': 'barks',
'': 'is silent',
}
),
),
E2eTestData(
'loop_nested',
dict(
animals=dict(
cats=dict(
Loki='black, white, red',
Judo='black, white',
),
dogs=dict(
Pluto='golden',
Goofy='black',
),
)
),
),
E2eTestData(
'loop_nested',
{},
error=True,
),
E2eTestData(
'missing_file',
{},
error=True,
),
]

adapters = {
'jinja': lambda template_root: JinjaAdapter(loader=jinja2.FileSystemLoader(template_root)),
'mako': MakoAdapter,
}

exclusions = {
'jinja': ['{{tmpl_dir}}/excluded_file.txt'],
'mako': ['${tmpl_dir}/excluded_file.txt'],
}

adapter_test_data = [
(*adapter, *param_set, exclusions[adapter[0]]) for adapter, param_set in product(adapters.items(), e2e_test_data)
]


@pytest.mark.parametrize('adapter_name,adapter,test_name,data,error,exclude', adapter_test_data)
def test_render(
adapter_name: str,
adapter: Callable[[Path], RendererAdapter],
test_name: str,
data: Mapping,
error: bool,
exclude: Iterable[str],
tmp_path: Path,
) -> None:
if adapter_name == 'mako' and sys.platform == 'win32':
pytest.skip('Mako has problem with line endings on windows')

root = Path(__file__).parent / 'test_render'
target_path = tmp_path / f'{adapter_name}_{test_name}'
target_path.mkdir()

def fn():
render(
adapter(root / 'templates' / adapter_name / test_name),
data,
target_path,
exclude_extend=exclude,
remove_suffixes=['.jinja', '.mako'],
)

if error:
with pytest.raises(RenderError):
fn()
else:
fn()
cmp_dirs(root / 'output' / test_name, target_path)
tmp_path,
remove_suffixes=['.jinja'],
report_cb=report_fb,
)

assert logs == {
(pathlib.PurePath('{{tmpl_file1}}'), pathlib.Path('file1.txt')),
(pathlib.PurePath('{{tmpl_file3}}'), pathlib.Path('subdir/file3.txt')),
(pathlib.PurePath('{{tmpl_dir}}/excluded_file.txt'), pathlib.Path('target_dir/excluded_file.txt')),
(pathlib.PurePath('{{tmpl_dir}}/suffixed.txt.jinja'), pathlib.Path('target_dir/suffixed.txt')),
(pathlib.PurePath('{{tmpl_dir}}/{{tmpl_file2}}'), pathlib.Path('target_dir/file2.txt')),
}

0 comments on commit 919e2e8

Please sign in to comment.