mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00

* feat: add benchmarking dashboard, CI hook on PR, and store lifetime results * refactor: change python env to 3.13 in benchmarks * refactor: add verbosity, use 3.11 for benchmarking * fix: OSError: [Errno 7] Argument list too long * refactor: add debug statements * refactor: remove extraneous -e * refactor: fix tests and linter errors * fix: track main package in coverage * refactor: fix test coverage testing * refactor: fix repo owner name in benchmark on pushing comment * refactor: add asv monkeypatch to docs workflow * refactor: temporarily allow building docs in forks * refactor: use py 3.13 for benchmarking * refactor: run only a single benchmark for PRs to speed them up * refactor: install asv in the docs build workflow * refactor: use hatch docs env to generate benhcmarks in docs CI * refactor: more trying * refactor: move tests * Add benchmark results for 0.137 * Trigger Build * Add benchmark results for 0.138 * refactor: set constant machine name when benchmarking * Add benchmark results for 0.139 * refactor: fix issue with paths too long * Add benchmark results for 0.140 * docs: update comment * refactor: remove test benchmarking data * refactor: fix comment * refactor: allow the benchmark workflow to write to PRs * refactor: use personal access token to set up the PR benchmark bot * refactor: split the benchmark PR flow into two to make it work with PRs from forks * refactor: update deprecated actions/upload-artifact@v3 to v4 * refactor: fix missing directory in benchmarking workflow * refactor: fix triggering of second workflow * refactor: fix workflow finally? * docs: add comments to cut-offs and direct people to benchmarks PR --------- Co-authored-by: github-actions <github-actions@github.com>
351 lines
11 KiB
Python
351 lines
11 KiB
Python
# NOTE: This file is used for benchmarking. Before editing this file,
|
|
# please read through these:
|
|
# - `benchmarks/README`
|
|
# - https://github.com/django-components/django-components/pull/999
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, Literal, NamedTuple, Optional, Union
|
|
|
|
import django
|
|
from django.conf import settings
|
|
from django.template import Context, Template
|
|
|
|
from django_components import Component, types
|
|
|
|
# DO NOT REMOVE - See https://github.com/django-components/django-components/pull/999
|
|
# ----------- IMPORTS END ------------ #
|
|
|
|
# This variable is overridden by the benchmark runner
|
|
CONTEXT_MODE: Literal["django", "isolated"] = "isolated"
|
|
|
|
if not settings.configured:
|
|
settings.configure(
|
|
BASE_DIR=Path(__file__).resolve().parent,
|
|
INSTALLED_APPS=["django_components"],
|
|
TEMPLATES=[
|
|
{
|
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
"DIRS": [
|
|
"tests/templates/",
|
|
"tests/components/", # Required for template relative imports in tests
|
|
],
|
|
"OPTIONS": {
|
|
"builtins": [
|
|
"django_components.templatetags.component_tags",
|
|
]
|
|
},
|
|
}
|
|
],
|
|
COMPONENTS={
|
|
"template_cache_size": 128,
|
|
"autodiscover": False,
|
|
"context_behavior": CONTEXT_MODE,
|
|
},
|
|
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
|
|
DATABASES={
|
|
"default": {
|
|
"ENGINE": "django.db.backends.sqlite3",
|
|
"NAME": ":memory:",
|
|
}
|
|
},
|
|
SECRET_KEY="secret",
|
|
ROOT_URLCONF="django_components.urls",
|
|
)
|
|
|
|
django.setup()
|
|
else:
|
|
settings.COMPONENTS["context_behavior"] = CONTEXT_MODE
|
|
|
|
#####################################
|
|
#
|
|
# IMPLEMENTATION START
|
|
#
|
|
#####################################
|
|
|
|
templates_cache: Dict[int, Template] = {}
|
|
|
|
|
|
def lazy_load_template(template: str) -> Template:
|
|
template_hash = hash(template)
|
|
if template_hash in templates_cache:
|
|
return templates_cache[template_hash]
|
|
else:
|
|
template_instance = Template(template)
|
|
templates_cache[template_hash] = template_instance
|
|
return template_instance
|
|
|
|
|
|
#####################################
|
|
# RENDER ENTRYPOINT
|
|
#####################################
|
|
|
|
|
|
def gen_render_data():
|
|
data = {
|
|
"href": "https://example.com",
|
|
"disabled": False,
|
|
"variant": "primary",
|
|
"type": "button",
|
|
"attrs": {
|
|
"class": "py-2 px-4",
|
|
},
|
|
}
|
|
return data
|
|
|
|
|
|
def render(data: Dict):
|
|
# Render
|
|
result = Button.render(
|
|
context=Context(),
|
|
kwargs=data,
|
|
slots={
|
|
"content": "Click me!",
|
|
},
|
|
)
|
|
return result
|
|
|
|
|
|
#####################################
|
|
# THEME
|
|
#####################################
|
|
|
|
ThemeColor = Literal["default", "error", "success", "alert", "info"]
|
|
ThemeVariant = Literal["primary", "secondary"]
|
|
|
|
VARIANTS = ["primary", "secondary"]
|
|
|
|
|
|
class ThemeStylingUnit(NamedTuple):
|
|
"""
|
|
Smallest unit of info, this class defines a specific styling of a specific
|
|
component in a specific state.
|
|
|
|
E.g. styling of a disabled "Error" button.
|
|
"""
|
|
|
|
color: str
|
|
"""CSS class(es) specifying color"""
|
|
css: str = ""
|
|
"""Other CSS classes not specific to color"""
|
|
|
|
|
|
class ThemeStylingVariant(NamedTuple):
|
|
"""
|
|
Collection of styling combinations that are meaningful as a group.
|
|
|
|
E.g. all "error" variants - primary, disabled, secondary, ...
|
|
"""
|
|
|
|
primary: ThemeStylingUnit
|
|
primary_disabled: ThemeStylingUnit
|
|
secondary: ThemeStylingUnit
|
|
secondary_disabled: ThemeStylingUnit
|
|
|
|
|
|
class Theme(NamedTuple):
|
|
"""Class for defining a styling and color theme for the app."""
|
|
|
|
default: ThemeStylingVariant
|
|
error: ThemeStylingVariant
|
|
alert: ThemeStylingVariant
|
|
success: ThemeStylingVariant
|
|
info: ThemeStylingVariant
|
|
|
|
|
|
_secondary_btn_styling = "ring-1 ring-inset"
|
|
|
|
theme = Theme(
|
|
default=ThemeStylingVariant(
|
|
primary=ThemeStylingUnit(
|
|
color="bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 transition"
|
|
),
|
|
primary_disabled=ThemeStylingUnit(color="bg-blue-300 text-blue-50 focus-visible:outline-blue-600 transition"),
|
|
secondary=ThemeStylingUnit(
|
|
color="bg-white text-gray-800 ring-gray-300 hover:bg-gray-100 focus-visible:outline-gray-600 transition",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
secondary_disabled=ThemeStylingUnit(
|
|
color="bg-white text-gray-300 ring-gray-300 focus-visible:outline-gray-600 transition",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
),
|
|
error=ThemeStylingVariant(
|
|
primary=ThemeStylingUnit(color="bg-red-600 text-white hover:bg-red-500 focus-visible:outline-red-600"),
|
|
primary_disabled=ThemeStylingUnit(color="bg-red-300 text-white focus-visible:outline-red-600"),
|
|
secondary=ThemeStylingUnit(
|
|
color="bg-white text-red-600 ring-red-300 hover:bg-red-100 focus-visible:outline-red-600",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
secondary_disabled=ThemeStylingUnit(
|
|
color="bg-white text-red-200 ring-red-100 focus-visible:outline-red-600",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
),
|
|
alert=ThemeStylingVariant(
|
|
primary=ThemeStylingUnit(color="bg-amber-500 text-white hover:bg-amber-400 focus-visible:outline-amber-500"),
|
|
primary_disabled=ThemeStylingUnit(color="bg-amber-100 text-orange-300 focus-visible:outline-amber-500"),
|
|
secondary=ThemeStylingUnit(
|
|
color="bg-white text-amber-500 ring-amber-300 hover:bg-amber-100 focus-visible:outline-amber-500",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
secondary_disabled=ThemeStylingUnit(
|
|
color="bg-white text-orange-200 ring-amber-100 focus-visible:outline-amber-500",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
),
|
|
success=ThemeStylingVariant(
|
|
primary=ThemeStylingUnit(color="bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600"),
|
|
primary_disabled=ThemeStylingUnit(color="bg-green-300 text-white focus-visible:outline-green-600"),
|
|
secondary=ThemeStylingUnit(
|
|
color="bg-white text-green-600 ring-green-300 hover:bg-green-100 focus-visible:outline-green-600",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
secondary_disabled=ThemeStylingUnit(
|
|
color="bg-white text-green-200 ring-green-100 focus-visible:outline-green-600",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
),
|
|
info=ThemeStylingVariant(
|
|
primary=ThemeStylingUnit(color="bg-sky-600 text-white hover:bg-sky-500 focus-visible:outline-sky-600"),
|
|
primary_disabled=ThemeStylingUnit(color="bg-sky-300 text-white focus-visible:outline-sky-600"),
|
|
secondary=ThemeStylingUnit(
|
|
color="bg-white text-sky-600 ring-sky-300 hover:bg-sky-100 focus-visible:outline-sky-600",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
secondary_disabled=ThemeStylingUnit(
|
|
color="bg-white text-sky-200 ring-sky-100 focus-visible:outline-sky-600",
|
|
css=_secondary_btn_styling,
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def get_styling_css(
|
|
variant: Optional["ThemeVariant"] = None,
|
|
color: Optional["ThemeColor"] = None,
|
|
disabled: Optional[bool] = None,
|
|
):
|
|
"""
|
|
Dynamically access CSS styling classes for a specific variant and state.
|
|
|
|
E.g. following two calls get styling classes for:
|
|
1. Secondary error state
|
|
1. Secondary alert disabled state
|
|
2. Primary default disabled state
|
|
```py
|
|
get_styling_css('secondary', 'error')
|
|
get_styling_css('secondary', 'alert', disabled=True)
|
|
get_styling_css(disabled=True)
|
|
```
|
|
"""
|
|
variant = variant or "primary"
|
|
color = color or "default"
|
|
disabled = disabled if disabled is not None else False
|
|
|
|
color_variants: ThemeStylingVariant = getattr(theme, color)
|
|
|
|
if variant not in VARIANTS:
|
|
raise ValueError(f'Unknown theme variant "{variant}", must be one of {VARIANTS}')
|
|
|
|
variant_name = variant if not disabled else f"{variant}_disabled"
|
|
styling: ThemeStylingUnit = getattr(color_variants, variant_name)
|
|
|
|
css = f"{styling.color} {styling.css}".strip()
|
|
return css
|
|
|
|
|
|
#####################################
|
|
# BUTTON
|
|
#####################################
|
|
|
|
|
|
class Button(Component):
|
|
def get_context_data(
|
|
self,
|
|
/,
|
|
*,
|
|
href: Optional[str] = None,
|
|
link: Optional[bool] = None,
|
|
disabled: Optional[bool] = False,
|
|
variant: Union["ThemeVariant", Literal["plain"]] = "primary",
|
|
color: Union["ThemeColor", str] = "default",
|
|
type: Optional[str] = "button",
|
|
attrs: Optional[dict] = None,
|
|
):
|
|
common_css = (
|
|
"inline-flex w-full text-sm font-semibold"
|
|
" sm:mt-0 sm:w-auto focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
)
|
|
if variant == "plain":
|
|
all_css_class = common_css
|
|
else:
|
|
button_classes = get_styling_css(variant, color, disabled) # type: ignore[arg-type]
|
|
all_css_class = f"{button_classes} {common_css} px-3 py-2 justify-center rounded-md shadow-sm"
|
|
|
|
is_link = not disabled and (href or link)
|
|
|
|
all_attrs = {**(attrs or {})}
|
|
if disabled:
|
|
all_attrs["aria-disabled"] = "true"
|
|
|
|
return {
|
|
"href": href,
|
|
"disabled": disabled,
|
|
"type": type,
|
|
"btn_class": all_css_class,
|
|
"attrs": all_attrs,
|
|
"is_link": is_link,
|
|
}
|
|
|
|
template: types.django_html = """
|
|
{# Based on buttons from https://tailwindui.com/components/application-ui/overlays/modals #}
|
|
|
|
{% if is_link %}
|
|
<a
|
|
href="{{ href }}"
|
|
{% html_attrs attrs class=btn_class class="no-underline" %}
|
|
>
|
|
{% else %}
|
|
<button
|
|
type="{{ type }}"
|
|
{% if disabled %} disabled {% endif %}
|
|
{% html_attrs attrs class=btn_class %}
|
|
>
|
|
{% endif %}
|
|
|
|
{% slot "content" default / %}
|
|
|
|
{% if is_link %}
|
|
</a>
|
|
{% else %}
|
|
</button>
|
|
{% endif %}
|
|
"""
|
|
|
|
|
|
#####################################
|
|
#
|
|
# IMPLEMENTATION END
|
|
#
|
|
#####################################
|
|
|
|
|
|
# DO NOT REMOVE - See https://github.com/django-components/django-components/pull/999
|
|
# ----------- TESTS START ------------ #
|
|
# The code above is used also used when benchmarking.
|
|
# The section below is NOT included.
|
|
|
|
from .testutils import CsrfTokenPatcher, GenIdPatcher # noqa: E402
|
|
|
|
def test_render(snapshot):
|
|
id_patcher = GenIdPatcher()
|
|
id_patcher.start()
|
|
csrf_token_patcher = CsrfTokenPatcher()
|
|
csrf_token_patcher.start()
|
|
|
|
data = gen_render_data()
|
|
rendered = render(data)
|
|
assert rendered == snapshot
|
|
|
|
id_patcher.stop()
|
|
csrf_token_patcher.stop()
|