Speedups, changes, typings

This commit is contained in:
László Károlyi 2024-09-13 01:02:30 +02:00
parent cb237826b9
commit 6b5d3554c4
Signed by: karolyi
GPG key ID: 2DCAF25E55735BFE
16 changed files with 106 additions and 49 deletions

0
py.typed Normal file
View file

View file

@ -46,6 +46,9 @@ setup(
keywords='python django boilerplate',
url='http://git.ksol.io/karolyi/py-ktools',
package_dir={'': 'src'},
package_data={'ktools.django': ['jinja/ktools/macros/*.html']},
package_data={
'': ['py.typed'],
'ktools.django': ['jinja/ktools/macros/*.html']
},
include_package_data=True,
license='MIT+NIGGER')

View file

@ -1,22 +1,32 @@
from functools import lru_cache, wraps
from functools import _lru_cache_wrapper, lru_cache, wraps
from typing import Callable, TypeVar
from weakref import ref
def memoized_method(*lru_args, **lru_kwargs):
'http://stackoverflow.com/a/33672499/1067833'
def decorator(func):
@wraps(func)
def wrapped_func(self, *args, **kwargs):
# We're storing the wrapped method inside the instance. If we had
# a strong reference to self the instance would be never
# garbage collected.
self_weak = ref(self)
_T = TypeVar(name='_T')
MemoizedCallable = Callable[..., _T]
def memoized_method(
maxsize: int | None = 128, typed: bool = False
) -> Callable[[MemoizedCallable], _lru_cache_wrapper]:
"""
LRU Cache decorator that keeps a weak reference to `self`, by
Raymond Hettinger: https://stackoverflow.com/a/68052994
"""
def wrapper(func: Callable[..., _T]):
@lru_cache(maxsize=maxsize, typed=typed)
def _func(_weakself, *args, **kwargs) -> _T:
return func(_weakself(), *args, **kwargs)
@wraps(func)
@lru_cache(*lru_args, **lru_kwargs)
def cached_method(*args, **kwargs):
return func(self_weak(), *args, **kwargs)
setattr(self, func.__name__, cached_method)
return cached_method(*args, **kwargs)
return wrapped_func
return decorator
def inner(self, *args, **kwargs) -> _T:
return _func(ref(self), *args, **kwargs)
setattr(inner, 'cache_info', _func.cache_info)
setattr(inner, 'cache_clear', _func.cache_clear)
setattr(inner, 'cache_parameters', _func.cache_parameters)
return inner
return wrapper

9
src/ktools/cache/functional.pyi vendored Normal file
View file

@ -0,0 +1,9 @@
from functools import _lru_cache_wrapper
from typing import Callable, TypeVar
_T = TypeVar('_T')
def memoized_method(
maxsize: int | None = 128, typed: bool = False
) -> Callable[[Callable[..., _T]], _lru_cache_wrapper[_T]]: ...

View file

@ -4,7 +4,7 @@ from django.template import engines
from .conf import setup_template_backend
from .conf.constants import TEMPLATE_ENGINE_NAME
from .jinja import templatetags
from .jinja.templatetags import template_getter
from .utils.translation import gettext_safelazy as _
@ -20,8 +20,10 @@ class KtoolsDjangoConfig(AppConfig):
Set up the instance of `KtoolsJinjaBackend` for later private
use.
"""
engines.templates[TEMPLATE_ENGINE_NAME] = \
setup_template_backend(DEBUG=settings.DEBUG)
templatetags._k_jinja_backend = engines[TEMPLATE_ENGINE_NAME]
backend_cfg = setup_template_backend(DEBUG=settings.DEBUG)
engines.templates[TEMPLATE_ENGINE_NAME] = backend_cfg
template_getter.setup(
backend=engines[TEMPLATE_ENGINE_NAME],
cache_enabled=backend_cfg['OPTIONS']['bytecode_cache']['enabled'])
del engines.templates[TEMPLATE_ENGINE_NAME]
del engines._engines[TEMPLATE_ENGINE_NAME]

View file

@ -21,7 +21,7 @@ def setup_template_backend(DEBUG: bool) -> _BackendType:
'DIRS': [],
'OPTIONS': {
# It's already got a sensible default but repeat it here
'app_dirname': 'ktools-jinjatemplates',
'app_dirname': 'ktemplates',
'match_extension': None,
'newstyle_gettext': True,
'extensions': DEFAULT_EXTENSIONS + [

View file

@ -1,6 +1,5 @@
from __future__ import annotations
from functools import lru_cache
from typing import Any
from django.forms.boundfield import BoundField
@ -15,16 +14,49 @@ from jinja2.utils import pass_context
from ktools.django.jinja.backend import KtoolsJinjaBackend
# See assignment in apps.py's `ready`
_k_jinja_backend: KtoolsJinjaBackend
class _TemplateGetter(object):
# See assignment in apps.py's `ready`
cache_enabled = False
k_jinja_backend: KtoolsJinjaBackend
@lru_cache(maxsize=None)
def _get_template(template_name: str) -> Template:
'Return (and cache) a `Template` to render.'
return _k_jinja_backend.get_template(
def _get_template(self, template_name: str) -> Template:
return self.k_jinja_backend.get_template(
template_name=template_name) # pyright: ignore[reportReturnType]
def setup(self, backend: KtoolsJinjaBackend, cache_enabled: bool):
self.k_jinja_backend = backend
self.cache_enabled = cache_enabled or True
if cache_enabled or True:
self.hits = self.misses = 0
self.cached = dict[str, Template]()
def stats(self) -> str:
if not self.cache_enabled:
return 'Cache not enabled.'
return (
f'Hits: {self.hits}, Misses: {self.misses}, '
f'Size: {len(self.cached)}')
def __call__(self, template_name: str) -> Template:
if not self.cache_enabled:
obj: Template = self.k_jinja_backend.get_template(
template_name=template_name
) # pyright: ignore[reportAssignmentType]
return obj
elif template := self.cached.get(template_name):
self.hits += 1
return template
self.misses += 1
obj: Template = self.k_jinja_backend.get_template(
template_name=template_name
) # pyright: ignore[reportAssignmentType]
self.cached[template_name] = obj
return obj
template_getter = _TemplateGetter()
@pass_context
def ktools_render_messages(
@ -33,8 +65,8 @@ def ktools_render_messages(
'Rendering a HTML message in various formats from ktools templates.'
match variant:
case 'bootstrap5-v1':
template = _get_template(template_name=(
'ktools/render-content/messages-bootstrap5-v1.html.jinja'))
template = template_getter(template_name=(
'render-content/messages-bootstrap5-v1.html.jinja'))
return template.render(context=context, request=request)
case _:
raise TemplateNotFound(
@ -48,27 +80,25 @@ _TEMPLATE_MAPPINGS = {
'bootstrap5-floating': {
'TextInput': {
'template_name': (
'ktools/widgets/bootstrap5/floating/input-textual.html.jinja')
'widgets/bootstrap5/floating/input-textual.html.jinja')
},
'EmailInput': {
'template_name': (
'ktools/widgets/bootstrap5/floating/input-textual.html.jinja')
'widgets/bootstrap5/floating/input-textual.html.jinja')
},
'RegionalPhoneNumberWidget': {
'template_name': (
'ktools/widgets/bootstrap5/floating/input-textual.html.jinja')
'widgets/bootstrap5/floating/input-textual.html.jinja')
},
'PasswordInput': {
'template_name': (
'ktools/widgets/bootstrap5/floating/input-textual.html.jinja')
'widgets/bootstrap5/floating/input-textual.html.jinja')
},
'CheckboxSelectMultiple': {
'template_name': (
'ktools/widgets/bootstrap5/floating/multiple-input.html'
'.jinja'),
'widgets/bootstrap5/floating/multiple-input.html.jinja'),
'option_template_name': (
'ktools/widgets/bootstrap5/floating/input-option.html'
'.jinja')
'widgets/bootstrap5/floating/input-option.html.jinja')
},
}
}
@ -87,7 +117,7 @@ class KTemplatesSetting(BaseRenderer):
Add the `_KBoundField` back to the context so that the template
can use it.
"""
template = _get_template(template_name=template_name)
template = template_getter(template_name=template_name)
context['_k_boundfield'] = self._k_boundfield
return template.render(context=context, request=request).strip()

View file

@ -12,7 +12,7 @@
{% else %}
{{- attr_class(widget, 'form-check-input') -}}
{% endif %}
{% include 'ktools/widgets/bootstrap5/floating/attrs-wo-class.html.jinja' %}
{% include 'widgets/bootstrap5/floating/attrs-wo-class.html.jinja' %}
placeholder="{{ _k_boundfield.label|e }}">
<label
{%- if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}

View file

@ -1,4 +1,4 @@
{% from 'ktools/widgets/bootstrap5/floating/attr-class.html.jinja' import attr_class with context %}
{% from 'widgets/bootstrap5/floating/attr-class.html.jinja' import attr_class with context %}
<div class="form-floating mb-3" data-ktools-field-type="{{ dash_case_convert(name=_k_boundfield.field.widget.__class__.__name__) }}">
<input type="{{ widget.type }}" name="{{ widget.name|e }}"
{%- if widget.value != None %}
@ -13,7 +13,7 @@
{% else %}
{{- attr_class(widget, 'form-control') -}}
{% endif %}
{% include 'ktools/widgets/bootstrap5/floating/attrs-wo-class.html.jinja' %}
{% include 'widgets/bootstrap5/floating/attrs-wo-class.html.jinja' %}
placeholder="{{ _k_boundfield.label|e }}">
<label for="{{ widget.attrs.id }}">{{ _k_boundfield.label }}</label>
{% if _k_boundfield.form.is_bound and _k_boundfield.errors %}

View file

@ -1,4 +1,4 @@
{% from 'ktools/widgets/bootstrap5/floating/attr-class.html.jinja' import attr_class with context %}
{% from 'widgets/bootstrap5/floating/attr-class.html.jinja' import attr_class with context %}
{% set id = widget.attrs.id %}
<div class="mb-3">
<fieldset data-ktools-field-type="{{ dash_case_convert(name=_k_boundfield.field.widget.__class__.__name__) }}"

View file

@ -87,6 +87,9 @@ class TokenLink(object):
return result
@cached_property
def url(self) -> str:
'Return the URL to use this `TokenLink`.'
def urlpath(self) -> str:
"""
Return the URL path to use this `TokenLink`. That is, without
`scheme` and `hostname`.
"""
return reverse(viewname=TOKENLINK_NS, kwargs=dict(token=self.token))