Skip to content

Commit

Permalink
implements #156
Browse files Browse the repository at this point in the history
  • Loading branch information
bckohan committed Jul 17, 2024
1 parent cc188da commit 237ea68
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 6 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020-2023 Brian Kohan
Copyright (c) 2020-2024 Brian Kohan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
5 changes: 5 additions & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Change Log
==========

v3.1.0 (16-JUL-2024)
====================

* Implemented `Support a no-render copy operation where the path may be a template. <https://github.com/bckohan/django-render-static/issues/156>`_

v3.0.1 (15-JUL-2024)
====================

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-render-static"
version = "3.0.1"
version = "3.1.0"
description = "Use Django's template engine to render static files at deployment or package time. Includes transpilers for extending Django's url reversal and enums to JavaScript."
authors = ["Brian Kohan <[email protected]>"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion render_static/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

VERSION = (3, 0, 1)
VERSION = (3, 1, 0)

__title__ = "Django Render Static"
__version__ = ".".join(str(i) for i in VERSION)
Expand Down
65 changes: 62 additions & 3 deletions render_static/engine.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from collections import Counter, namedtuple
from pathlib import Path
from shutil import copy2
from typing import Callable, Dict, Generator, List, Optional, Tuple, Union, cast

from django.conf import settings
Expand Down Expand Up @@ -453,6 +454,8 @@ def render_to_disk(
first_engine: bool = False,
first_loader: bool = False,
first_preference: bool = False,
exclude: Optional[List[Path]] = None,
render_contents: bool = True,
) -> List[Render]:
"""
Wrap render_each generator function and return the whole list of
Expand Down Expand Up @@ -480,6 +483,12 @@ def render_to_disk(
first_loader will render only the first preference(s) of the first
loader. Preferences are loader specific and documented on the
loader.
:param exclude: A list of template paths to exclude. If the path is a
directory, any template below that directory will be excluded. This
parameter only makes sense to use if your selector is a glob pattern.
:param render_contents: If False, do not render the contents of the template.
If the destination path is a template it will still be rendered against
the context to produce the final path.
:return: Render object for all the template(s) rendered to disk
:raises TemplateDoesNotExist: if no template by the given name is found
:raises ImproperlyConfigured: if not enough information was given to
Expand All @@ -494,6 +503,8 @@ def render_to_disk(
first_engine=first_engine,
first_loader=first_loader,
first_preference=first_preference,
exclude=exclude,
render_contents=render_contents,
)
]

Expand All @@ -504,6 +515,7 @@ def find(
first_engine: bool = False,
first_loader: bool = False,
first_preference: bool = False,
exclude: Optional[List[Path]] = None,
) -> Generator[Render, None, None]:
"""
Search for all templates that match the given selectors and yield
Expand All @@ -514,6 +526,9 @@ def find(
:param first_engine: See render_each
:param first_loader: See render_each
:param first_preference: See render_each
:param exclude: A list of template paths to exclude. If the path is a
directory, any template below that directory will be excluded. This
parameter only makes sense to use if your selector is a glob pattern.
:yield: Render objects for each template to disk
:raises TemplateDoesNotExist: if no template by the given name is found
"""
Expand All @@ -535,6 +550,7 @@ def find(
first_engine=first_engine,
first_loader=first_loader,
first_preference=first_preference,
exclude=exclude,
)

def search(
Expand Down Expand Up @@ -566,6 +582,8 @@ def render_each(
first_engine: bool = False,
first_loader: bool = False,
first_preference: bool = False,
exclude: Optional[List[Path]] = None,
render_contents: bool = True,
) -> Generator[Render, None, None]:
"""
A generator function that renders all selected templates of the highest
Expand Down Expand Up @@ -597,6 +615,12 @@ def render_each(
first_loader will render only the first preference(s) of the first
loader. Preferences are loader specific and documented on the
loader.
:param exclude: A list of template paths to exclude. If the path is a
directory, any template below that directory will be excluded. This
parameter only makes sense to use if your selector is a glob pattern.
:param render_contents: If False, do not render the contents of the template.
If the destination path is a template it will still be rendered against
the context to produce the final path.
:yield: Render objects for each template to disk
:raises TemplateDoesNotExist: if no template by the given name is found
:raises ImproperlyConfigured: if not enough information was given to
Expand All @@ -611,6 +635,7 @@ def render_each(
first_engine=first_engine,
first_loader=first_loader,
first_preference=first_preference,
exclude=exclude,
):
ctx = render.config.context.copy()
if context is not None:
Expand All @@ -623,12 +648,20 @@ def render_each(
os.makedirs(str(dest), exist_ok=True)
else:
os.makedirs(Path(dest or "").parent, exist_ok=True)
with open(str(dest), "w", encoding="UTF-8") as out:
out.write(render.template.render(r_ctx))
if render_contents:
with open(str(dest), "w", encoding="UTF-8") as out:
out.write(render.template.render(r_ctx))
else:
copy2(Path(render.template.origin.name), Path(dest))
yield render

def resolve_renderings(
self, selector: str, config: TemplateConfig, batch: bool, **kwargs
self,
selector: str,
config: TemplateConfig,
batch: bool,
exclude: Optional[List[Path]] = None,
**kwargs,
) -> Generator[Render, None, None]:
"""
Resolve the given parameters to a or a set of Render objects containing
Expand All @@ -637,11 +670,23 @@ def resolve_renderings(
:param selector: The template selector (name string)
:param config: The TemplateConfig to apply to the selector.
:param batch: True if this is a batch rendering, false otherwise.
:param exclude: A list of template paths to exclude. If the path is a
directory, any template below that directory will be excluded. This
parameter only makes sense to use if your selector is a glob pattern.
:param kwargs: Pass through parameters from render_each
:yield: Render objects
"""
templates: Dict[str, DjangoTemplate] = {}
chain = []

excluded_dirs = []
excluded_files = []
for xcl in exclude or []:
if xcl.is_dir():
excluded_dirs.append(xcl.absolute())
else:
excluded_files.append(xcl.absolute())

for engine in self.all():
try:
for template_name in engine.select_templates(
Expand Down Expand Up @@ -675,6 +720,20 @@ def resolve_renderings(
raise TemplateDoesNotExist(selector, chain=chain)

for _, template in templates.items():
if any(
(
Path(template.origin.name).absolute().is_relative_to(excl)
for excl in excluded_dirs
)
):
continue
if any(
(
Path(template.origin.name).absolute() == excl
for excl in excluded_files
)
):
continue
yield Render(
selector=selector,
config=config,
Expand Down
24 changes: 24 additions & 0 deletions render_static/management/commands/renderstatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,28 @@ def handle(
),
),
] = False,
exclude: Annotated[
t.List[Path],
Option(
"--exclude",
"-e",
help=_(
"Exclude these files from rendering, or any files at or below "
"this directory."
),
shell_complete=complete_path,
),
] = [],
no_render_contents: Annotated[
bool,
Option(
"--no-render-contents",
help=_(
"Do not render the contents of the files. If paths are "
"templates, destinations will still be rendered."
),
),
] = False,
):
engine = StaticTemplateEngine()

Expand All @@ -171,6 +193,8 @@ def handle(
first_engine=first_engine,
first_loader=first_loader,
first_preference=first_preference,
exclude=exclude,
render_contents=not no_render_contents,
):
self.stdout.write(
self.style.SUCCESS(_("Rendered {render}.").format(render=render))
Expand Down
1 change: 1 addition & 0 deletions tests/batch_templates/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file.txt: {{ variable }}
1 change: 1 addition & 0 deletions tests/batch_templates/folder1/file1_1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file1_1.txt: {{ variable }}
1 change: 1 addition & 0 deletions tests/batch_templates/folder1/file1_2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file1_2.txt: {{ variable }}
1 change: 1 addition & 0 deletions tests/batch_templates/folder2/file2_1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file2_1.txt: {{ variable }}
1 change: 1 addition & 0 deletions tests/batch_templates/folder2/file2_2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file2_2.txt: {{ variable }}
106 changes: 106 additions & 0 deletions tests/test_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from pathlib import Path

from django.core.management import call_command
from django.test import override_settings
from .test_core import BaseTestCase, GLOBAL_STATIC_DIR

BATCH_TEMPLATES = Path(__file__).parent / "batch_templates"

context = {"variable": "template_rendered!"}


@override_settings(
STATIC_TEMPLATES={
"ENGINES": [
{
"BACKEND": "render_static.backends.StaticDjangoTemplates",
"DIRS": [BATCH_TEMPLATES],
"APP_DIRS": False,
"OPTIONS": {
"loaders": ["render_static.loaders.StaticFilesystemBatchLoader"]
},
}
]
}
)
class TestBatch(BaseTestCase):
def check_file(self, filename, rendered=True, exists=True):
filename = GLOBAL_STATIC_DIR / filename
if exists:
self.assertTrue(filename.is_file())
if rendered:
self.assertEqual(
filename.read_text().strip(),
f"{filename.name}: {context["variable"]}",
)
else:
self.assertEqual(
filename.read_text().strip(), f"{filename.name}: {{{{ variable }}}}"
)
else:
self.assertFalse(filename.is_file())

def test_batch_glob_all_render_all(self):
call_command(
"renderstatic",
"**/*",
destination=GLOBAL_STATIC_DIR,
context="tests.test_batch.context",
)

self.check_file("file.txt")
self.check_file("folder1/file1_1.txt")
self.check_file("folder1/file1_2.txt")
self.check_file("folder2/file2_1.txt")
self.check_file("folder2/file2_2.txt")

def test_batch_glob_some_render_all(self):
call_command(
"renderstatic",
"folder1/**",
destination=GLOBAL_STATIC_DIR,
context="tests.test_batch.context",
)

self.check_file("file.txt", exists=False)
self.check_file("folder1/file1_1.txt")
self.check_file("folder1/file1_2.txt")
self.check_file("folder2/file2_1.txt", exists=False)
self.check_file("folder2/file2_2.txt", exists=False)

def test_batch_glob_all_exclude_some(self):
call_command(
"renderstatic",
"**/*",
destination=GLOBAL_STATIC_DIR,
context="tests.test_batch.context",
exclude=[BATCH_TEMPLATES / "file.txt", BATCH_TEMPLATES / "folder1"],
)

self.check_file("file.txt", exists=False)
self.check_file("folder1/file1_1.txt", exists=False)
self.check_file("folder1/file1_2.txt", exists=False)
self.check_file("folder2/file2_1.txt")
self.check_file("folder2/file2_2.txt")

def test_batch_glob_all_exclude_some_no_render(self):
call_command(
"renderstatic",
"**/*",
destination=GLOBAL_STATIC_DIR,
context="tests.test_batch.context",
exclude=[BATCH_TEMPLATES / "folder2"],
)
call_command(
"renderstatic",
"folder2/*",
destination=GLOBAL_STATIC_DIR,
context="tests.test_batch.context",
no_render_contents=True,
)

self.check_file("file.txt")
self.check_file("folder1/file1_1.txt")
self.check_file("folder1/file1_2.txt")
self.check_file("folder2/file2_1.txt", rendered=False)
self.check_file("folder2/file2_2.txt", rendered=False)
27 changes: 27 additions & 0 deletions tests/test_batch_jinja2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest
from django.test import override_settings

try:
from render_static.backends.jinja2 import StaticJinja2Templates
from render_static.loaders.jinja2 import StaticFileSystemBatchLoader
except ImportError:
pytest.skip(allow_module_level=True, reason="Jinja2 is not installed")


from .test_batch import TestBatch, BATCH_TEMPLATES


@override_settings(
STATIC_TEMPLATES={
"ENGINES": [
{
"BACKEND": "render_static.backends.jinja2.StaticJinja2Templates",
"OPTIONS": {
"loader": StaticFileSystemBatchLoader(searchpath=BATCH_TEMPLATES)
},
}
]
}
)
class TestBatchJinja2(TestBatch):
pass

0 comments on commit 237ea68

Please sign in to comment.