Skip to content

Commit

Permalink
Refactor regex_replacer to split up responsibilities.
Browse files Browse the repository at this point in the history
  • Loading branch information
adamghill committed Nov 14, 2024
1 parent 614f263 commit 9e87578
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 104 deletions.
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from django.conf import settings

from dj_angles.regex_replacer import clear_tag_map
from dj_angles.mappers.mapper import clear_tag_map


def pytest_configure():
Expand Down
4 changes: 4 additions & 0 deletions src/dj_angles/mappers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from dj_angles.mappers.django import map_autoescape, map_block, map_css, map_extends, map_image
from dj_angles.mappers.include import map_include
from dj_angles.mappers.mapper import get_tag_map
from dj_angles.mappers.thirdparty import map_bird
2 changes: 1 addition & 1 deletion src/dj_angles/mappers/angles.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def default_mapper(tag: "Tag") -> str:


def map_angles_include(tag: "Tag") -> str:
"""Mapper function for the angles include tag which is custom to handle slots.
"""Mapper function for the angles include tag; handles the implementation of slots.
Args:
param tag: The tag to map.
Expand Down
75 changes: 75 additions & 0 deletions src/dj_angles/mappers/mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from collections.abc import Callable
from typing import Optional, Union

from django.utils.module_loading import import_string

from dj_angles.mappers import map_autoescape, map_block, map_css, map_extends, map_image, map_include
from dj_angles.modules import is_module_available
from dj_angles.settings import get_setting

TAG_NAME_TO_DJANGO_TEMPLATE_TAG_MAP: Optional[dict[Optional[str], Union[Callable, str]]] = {
"extends": map_extends,
"block": map_block,
"verbatim": "verbatim",
"include": map_include,
"comment": "comment",
"#": "comment",
"autoescape-on": map_autoescape,
"autoescape-off": map_autoescape,
"csrf-token": "csrf_token",
"csrf": "csrf_token",
"csrf-input": "csrf_token",
"debug": "debug",
"filter": "filter",
"lorem": "lorem",
"now": "now",
"spaceless": "spaceless",
"templatetag": "templatetag",
"image": map_image,
"css": map_css,
}
"""Default mappings for tag names to Django template tags."""

tag_map: Optional[dict[Optional[str], Union[Callable, str]]] = None


def get_tag_map() -> Optional[dict[Optional[str], Union[Callable, str]]]:
"""Get the complete tag map based on the default, dynamic, and settings mappers."""

global tag_map # noqa: PLW0603

if tag_map is None:
tag_map = TAG_NAME_TO_DJANGO_TEMPLATE_TAG_MAP

if tag_map is None:
raise AssertionError("Invalid tag_map")

# Add bird if installed
if is_module_available("django_bird"):
# Import here to avoid circular import
from dj_angles.mappers import map_bird

tag_map.update({"bird": map_bird})

# Add dynamic mappers if in settings
mappers = get_setting("mappers", default={})

if not isinstance(mappers, dict):
raise AssertionError("ANGLES.mappers must be a dictionary")

tag_map.update(mappers)

# Add default mapper if in settings, or fallback to the default mapper
default_mapper = get_setting("default_mapper", "dj_angles.mappers.angles.default_mapper")

# Add the default with a magic key of `None`
tag_map.update({None: import_string(default_mapper)})

return tag_map


def clear_tag_map() -> None:
"""Clear the generated tag map so that it will be re-generated. Useful for tests."""

global tag_map # noqa: PLW0603
tag_map = None
7 changes: 7 additions & 0 deletions src/dj_angles/modules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from importlib.util import find_spec


def is_module_available(module_name):
"""Helper method to check if a module is available."""

return find_spec(module_name) is not None
102 changes: 3 additions & 99 deletions src/dj_angles/regex_replacer.py
Original file line number Diff line number Diff line change
@@ -1,109 +1,13 @@
import re
from collections import deque
from collections.abc import Callable
from functools import lru_cache
from importlib.util import find_spec
from typing import Optional, Union

from django.utils.module_loading import import_string
from minestrone import HTML

from dj_angles.exceptions import InvalidEndTagError
from dj_angles.mappers.django import map_autoescape, map_block, map_css, map_extends, map_image
from dj_angles.mappers.include import map_include
from dj_angles.mappers.thirdparty import map_bird
from dj_angles.settings import get_setting
from dj_angles.mappers import get_tag_map
from dj_angles.settings import get_setting, get_tag_regex
from dj_angles.tags import Tag

TAG_NAME_TO_DJANGO_TEMPLATE_TAG_MAP: Optional[dict[Optional[str], Union[Callable, str]]] = {
"extends": map_extends,
"block": map_block,
"verbatim": "verbatim",
"include": map_include,
"comment": "comment",
"#": "comment",
"autoescape-on": map_autoescape,
"autoescape-off": map_autoescape,
"csrf-token": "csrf_token",
"csrf": "csrf_token",
"csrf-input": "csrf_token",
"debug": "debug",
"filter": "filter",
"lorem": "lorem",
"now": "now",
"spaceless": "spaceless",
"templatetag": "templatetag",
"image": map_image,
"css": map_css,
}
"""Default mappings for tag names to Django template tags."""

tag_map: Optional[dict[Optional[str], Union[Callable, str]]] = None


def _is_module_available(module_name):
"""Helper method to check if a module is available."""

return find_spec(module_name) is not None


def _get_tag_regex():
"""Gets a compiled regex based on the `initial_tag_regex` setting or default of r'(dj-)'."""

initial_tag_regex = get_setting("initial_tag_regex", default=r"(dj-)")

if initial_tag_regex is None:
initial_tag_regex = ""

tag_regex = rf"</?({initial_tag_regex}(?P<tag_name>[^\s>]+))\s*(?P<template_tag_args>.*?)\s*/?>"

@lru_cache(maxsize=32)
def _compile_regex(_tag_regex):
"""Silly internal function to cache the compiled regex."""

return re.compile(_tag_regex)

return _compile_regex(tag_regex)


def get_tag_map() -> Optional[dict[Optional[str], Union[Callable, str]]]:
"""Get the complete tag map based on the default, dynamic, and settings mappers."""

global tag_map # noqa: PLW0603

if tag_map is None:
tag_map = TAG_NAME_TO_DJANGO_TEMPLATE_TAG_MAP

if tag_map is None:
raise AssertionError("Invalid tag_map")

# Add bird if installed
if _is_module_available("django_bird"):
tag_map.update({"bird": map_bird})

# Add dynamic mappers if in settings
mappers = get_setting("mappers", default={})

if not isinstance(mappers, dict):
raise AssertionError("ANGLES.mappers must be a dictionary")

tag_map.update(mappers)

# Add default mapper if in settings, or fallback to the default mapper
default_mapper = get_setting("default_mapper", "dj_angles.mappers.angles.default_mapper")

# Add the default with a magic key of `None`
tag_map.update({None: import_string(default_mapper)})

return tag_map


def clear_tag_map() -> None:
"""Clear the generated tag map so that it will be re-generated. Useful for tests."""

global tag_map # noqa: PLW0603
tag_map = None


def get_replacements(html: str, *, raise_for_missing_start_tag: bool = True) -> list[tuple[str, str]]:
"""Get a list of replacements (tuples that consists of 2 strings) based on the template HTML.
Expand All @@ -118,7 +22,7 @@ def get_replacements(html: str, *, raise_for_missing_start_tag: bool = True) ->
"""

replacements = []
tag_regex = _get_tag_regex()
tag_regex = get_tag_regex()
tag_queue: deque = deque()

tag_map = get_tag_map()
Expand Down
21 changes: 21 additions & 0 deletions src/dj_angles/settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re
from functools import lru_cache
from typing import Any

from django.conf import settings
Expand All @@ -18,3 +20,22 @@ def get_setting(setting_name: str, default=None) -> Any:
return settings.ANGLES[setting_name]

return default


def get_tag_regex():
"""Gets a compiled regex based on the `initial_tag_regex` setting or default of r'(dj-)'."""

initial_tag_regex = get_setting("initial_tag_regex", default=r"(dj-)")

if initial_tag_regex is None:
initial_tag_regex = ""

tag_regex = rf"</?({initial_tag_regex}(?P<tag_name>[^\s>]+))\s*(?P<template_tag_args>.*?)\s*/?>"

@lru_cache(maxsize=32)
def _compile_regex(_tag_regex):
"""Silly internal function to cache the compiled regex."""

return re.compile(_tag_regex)

return _compile_regex(tag_regex)
3 changes: 2 additions & 1 deletion tests/dj_angles/regex_replacer/test_get_replacements.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import pytest

from dj_angles.regex_replacer import clear_tag_map, get_replacements
from dj_angles.mappers.mapper import clear_tag_map
from dj_angles.regex_replacer import get_replacements

# Structure to store parameterize data
Params = namedtuple(
Expand Down
5 changes: 3 additions & 2 deletions tests/dj_angles/tags/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import re

from dj_angles.regex_replacer import _get_tag_regex, get_tag_map
from dj_angles.mappers.mapper import get_tag_map
from dj_angles.settings import get_tag_regex
from dj_angles.tags import Tag


def create_tag(html):
tag_regex = _get_tag_regex()
tag_regex = get_tag_regex()
match = re.match(tag_regex, html)

tag_html = html[match.start() : match.end()]
Expand Down

0 comments on commit 9e87578

Please sign in to comment.