Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/meta tag to cancel rendering #4

Merged
merged 2 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.0] - 2024-05-30

### Added

- New Extension that adds a `{% skip_if %}` tag where you can insert any
condition (e.g. checking the Context) and if it evals as True, a
`CancelRendering` exception is raised and caught by the Renderers causing
them to silently just skip rendering that template.
- The `StringRenderer` returns `None`
- The `StoutRenderer` prints out nothing
- The `FileRenderer` doesn't create an output file
- Added unittests for the `{% skip_if %}` tag


## [0.3.0] - 2024-05-28

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion ccpstencil/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '0.3.0'
__version__ = '0.4.0'

__author__ = 'Thordur Matthiasson <[email protected]>'
__license__ = 'MIT License'
Expand Down
1 change: 1 addition & 0 deletions ccpstencil/jinjaext/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from ._embed import *
from ._skip_if import *
21 changes: 21 additions & 0 deletions ccpstencil/jinjaext/extensions/_skip_if.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
__all__ = [
'SkipIfExtension',
]
from jinja2 import nodes
from jinja2.ext import Extension
from ccpstencil.structs import *


class SkipIfExtension(Extension):
tags = {'skip_if'}

def parse(self, parser):
lineno = next(parser.stream).lineno
# Parse the condition expression
condition = parser.parse_expression()
return nodes.CallBlock(self.call_method('_check_condition', [condition]), [], [], []).set_lineno(lineno)

def _check_condition(self, condition, caller):
if condition:
raise CancelRendering("Rendering cancelled due to meta_only_render_if condition being False")
return ''
19 changes: 14 additions & 5 deletions ccpstencil/renderer/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,22 @@ def template(self, value: ITemplate):
value.set_renderer(self)
self._template = value

@abc.abstractmethod
def render(self):
"""This should just be called by subclasses via super().render() in
order to run preflight and common stuff, but the empty return value
should be ignored.
"""
self._pre_flight()
try:
rendered_string = self._render_as_string()
return self._output_rendered_results(rendered_string)
except CancelRendering:
log.info(f'Rendering cancelled by skip_if tag')
return None

@abc.abstractmethod
def _output_rendered_results(self, rendered_string: str):
pass

@abc.abstractmethod
def _render_as_string(self) -> str:
pass

@property
def jinja_environment(self) -> jinja2.Environment:
Expand Down
14 changes: 12 additions & 2 deletions ccpstencil/renderer/_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ def __init__(self, output_file: Union[str, Path],
super().__init__(context, template, **kwargs)

def render(self) -> str:
return super().render()

def _output_rendered_results(self, rendered_string: str) -> str:
results = super().render()
if results is None:
return f'Skipped: {self._output_file.absolute()}'

if self._output_file.exists() and not self._overwrite:
raise OutputFileExistsError(f'The target output file already exists and overwriting is disabled: {self._output_file.absolute()}')
raise OutputFileExistsError(f'The target output file already exists and overwriting is'
f' disabled: {self._output_file.absolute()}')
self._output_file.parent.mkdir(parents=True, exist_ok=True)

with open(self._output_file, 'w') as fout:
fout.write(super().render())
fout.write(results)
return str(self._output_file.absolute())

8 changes: 5 additions & 3 deletions ccpstencil/renderer/_stdout.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
'StdOutRenderer',
]


from ccpstencil.structs import *
from ._string import *


class StdOutRenderer(StringRenderer):
def render(self):
print(super().render())
def render(self) -> NoReturn:
results = super().render()
if results is not None:
print(results)
9 changes: 6 additions & 3 deletions ccpstencil/renderer/_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ class StringRenderer(_BaseRenderer):
def __init__(self, context: Optional[IContext] = None, template: Optional[ITemplate] = None, **kwargs):
super().__init__(context, template, **kwargs)

def render(self) -> str:
super().render()
return self.template.get_jinja_template().render(**self.context.as_dict())
def render(self) -> Optional[str]:
return super().render()

def _render_as_string(self) -> str:
return self.template.get_jinja_template().render(**self.context.as_dict())

def _output_rendered_results(self, rendered_string: str) -> str:
return rendered_string
6 changes: 6 additions & 0 deletions ccpstencil/structs/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
'InvalidTemplateTypeForRendererError',
'OutputFileExistsError',
'EmbedFileNotFound',

'CancelRendering',
]


Expand Down Expand Up @@ -53,3 +55,7 @@ class OutputFileExistsError(RenderError, FileExistsError):

class EmbedFileNotFound(RenderError, FileNotFoundError):
pass


class CancelRendering(Exception):
pass
10 changes: 10 additions & 0 deletions tests/res/skipif/context.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mode:
normal:
adjective: Now
positive:
enabled: true
adjective: Shiny
negative:
enabled: false
adjective: Shitty
number: 7
2 changes: 2 additions & 0 deletions tests/res/skipif/template_negative.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% skip_if not mode.negative.enabled %}
The day is {{mode.negative.adjective}}
1 change: 1 addition & 0 deletions tests/res/skipif/template_normal.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The day is {{mode.normal.adjective}}
2 changes: 2 additions & 0 deletions tests/res/skipif/template_positive.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% skip_if not mode.positive.enabled %}
The day is {{mode.positive.adjective}}
2 changes: 2 additions & 0 deletions tests/res/skipif/template_seven.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% skip_if number == 7 %}
Seven!
2 changes: 2 additions & 0 deletions tests/res/skipif/template_six.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% skip_if number == 6 %}
Six!
45 changes: 45 additions & 0 deletions tests/test_skip_if.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import unittest

from ccpstencil.stencils import *

import os
import pathlib

import logging
logging.basicConfig(level=logging.DEBUG)


_HERE = pathlib.Path(__file__).parent.resolve()


def fp(file_name: str) -> str:
return str((_HERE / 'res/skipif/' / file_name).absolute())


class TestSkipIf(unittest.TestCase):
@classmethod
def setUpClass(cls):
os.environ['STENCIL_TEMPLATE_PATH'] = str(_HERE / 'res/skipif/')

def test_normal(self):
res = render_stencil('template_normal.txt', fp('context.yaml'))
self.assertEqual('The day is Now', res)

def test_skipped(self):
res = render_stencil('template_negative.txt', fp('context.yaml'))
self.assertIsNone(res)

def test_not_skipped(self):
res = render_stencil('template_positive.txt', fp('context.yaml'))
self.assertEqual('The day is Shiny', res)

def test_numbers(self):
# res = render_stencil('template_six.txt', fp('context.yaml'))
# self.assertEqual('Six!', res)
res = render_stencil('template_seven.txt', fp('context.yaml'))
self.assertIsNone(res)

@classmethod
def tearDownClass(cls):
if 'STENCIL_TEMPLATE_PATH' in os.environ:
del os.environ['STENCIL_TEMPLATE_PATH']
Loading