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

Add type hints #18

Merged
merged 35 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8a47a09
Add mypy as dev dependency
tbrlpld Feb 3, 2024
a0dbc87
Add django-stubs
tbrlpld Feb 3, 2024
7b1d2ea
Configure django-stubs
tbrlpld Feb 3, 2024
fad7852
Default to checking all files in the repo
tbrlpld Feb 3, 2024
1075ce3
Fix import location of conditional_escape
tbrlpld Feb 3, 2024
d9326fd
Update Media and MediaDefiningClass import location
tbrlpld Feb 3, 2024
80c7844
Use more concrete type
tbrlpld Feb 3, 2024
39cee94
Declare template_name property
tbrlpld Feb 3, 2024
8de1629
Update SafeString import source
tbrlpld Feb 3, 2024
27762d3
Enable mypy strict mode
tbrlpld Feb 3, 2024
ccf1c93
Fix annytations in testmanage.py
tbrlpld Feb 3, 2024
60a61f3
Add type hint for memebers of MediaContainer
tbrlpld Feb 3, 2024
faa4888
Add type hints to templatetag module
tbrlpld Feb 3, 2024
ac4a782
Fix type annotations for Python 3.8
tbrlpld Feb 3, 2024
f3f9440
Move protocols to separate module
tbrlpld Feb 3, 2024
d706241
Add return types to tests
tbrlpld Feb 4, 2024
e875a3d
Fix function signatures
tbrlpld Feb 4, 2024
0d249b6
Add type hint for media property
tbrlpld Feb 6, 2024
f5498c5
Use Media.__repr__ for equality assertion
tbrlpld Feb 6, 2024
c990aba
Use module-level imports to avoid name conflict
tbrlpld Feb 6, 2024
426e0b4
Annotate copying mock call method
tbrlpld Feb 6, 2024
1ef3f1d
Add return types
tbrlpld Feb 6, 2024
ae8b02a
Make method a mock with override instead of assignment
tbrlpld Feb 6, 2024
308fb07
Add missing function signatures
tbrlpld Feb 6, 2024
0915844
Add type hints to example components
tbrlpld Feb 10, 2024
0f7974a
Annotate view function
tbrlpld Feb 10, 2024
124969d
Annotate test functions
tbrlpld Feb 10, 2024
8d56158
Add mypy pre-commit hook
tbrlpld Feb 10, 2024
5816e1d
Add requests type anotations to enable mypy on all files
tbrlpld Feb 10, 2024
bbd6246
Install test dependencies for linting step
tbrlpld Feb 10, 2024
33a1031
Fix type hints for older Python versions
tbrlpld Feb 10, 2024
e8a47ab
Ignore typing-only code when computing test coverage
tbrlpld Feb 10, 2024
8078cdb
Update changelog
tbrlpld Feb 10, 2024
a91ed5c
Wrap type-check only imported name in quotes
tbrlpld Feb 10, 2024
98df83e
Add self code review check
tbrlpld Feb 10, 2024
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
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ exclude_lines =
if 0:
if __name__ == .__main__.:

# Don't complain about if code meant only for type checking isn't run:
if TYPE_CHECKING:
class .*\bProtocol\):

ignore_errors = True
show_missing = True
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
**Checklist**
<-- For each of the following: check `[x]` if fulfilled or mark as irrelevant `[-]` if not applicable. -->
- [ ] [CHANGELOG.md](../CHANGELOG.md) has been updated.
- [ ] Self code reviewed.
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ jobs:
with:
python-version: '3.11'
- name: Install
# Installing test dependencies to make sure that all imports work during type-checking.
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install .[dev]
python -m pip install .[testing,dev]
- uses: pre-commit/[email protected]

test:
Expand Down
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ repos:
entry: flake8
language: system
types: [python]
- id: mypy
name: mypy
entry: mypy
language: system
types: [python]
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Add more tests and example usage. ([#6](https://github.com/tbrlpld/laces/pull/6))
- Added support for Python 3.12 and Django 5.0. ([#15](https://github.com/tbrlpld/laces/pull/15))
- Added type hints and type checking with `mypy` in CI. ([#18](https://github.com/tbrlpld/laces/pull/18))

### Changed

Expand Down
42 changes: 35 additions & 7 deletions laces/components.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
from typing import Any, MutableMapping
from typing import TYPE_CHECKING, List

from django.forms import Media, MediaDefiningClass
from django.forms.widgets import Media, MediaDefiningClass
from django.template import Context
from django.template.loader import get_template

from laces.typing import HasMediaProperty


if TYPE_CHECKING:
from typing import Optional

from django.utils.safestring import SafeString

from laces.typing import RenderContext


class Component(metaclass=MediaDefiningClass):
"""
Expand All @@ -18,7 +28,12 @@ class Component(metaclass=MediaDefiningClass):
See also: https://docs.djangoproject.com/en/4.2/topics/forms/media/
"""

def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str:
template_name: str

def render_html(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "SafeString":
"""
Return string representation of the object.

Expand All @@ -40,12 +55,25 @@ def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str:
return template.render(context_data)

def get_context_data(
self, parent_context: MutableMapping[str, Any]
) -> MutableMapping[str, Any]:
self,
parent_context: "RenderContext",
) -> "Optional[RenderContext]":
return {}

# fmt: off
if TYPE_CHECKING:
# It's ugly, I know. But it seems to be the best way to make `mypy` happy.
# The `media` property is dynamically added by the `MediaDefiningClass`
# metaclass. Because of how dynamic it is, `mypy` is not able to pick it up.
# This is why we need to add a type hint for it here. The other way would be a
# stub, but that would require the whole module to be stubbed and that is even
# more annoying to keep up to date.
@property
def media(self) -> Media: ... # noqa: E704
# fmt: on


class MediaContainer(list):
class MediaContainer(List[HasMediaProperty]):
"""
A list that provides a `media` property that combines the media definitions
of its members.
Expand All @@ -64,7 +92,7 @@ class MediaContainer(list):
"""

@property
def media(self):
def media(self) -> Media:
"""
Return a `Media` object containing the media definitions of all members.

Expand Down
33 changes: 22 additions & 11 deletions laces/templatetags/laces.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
from typing import TYPE_CHECKING

from django import template
from django.template.base import token_kwargs
from django.template.defaultfilters import conditional_escape
from django.utils.html import conditional_escape
from django.utils.safestring import SafeString


if TYPE_CHECKING:
from typing import Optional

from django.template.base import FilterExpression, Parser, Token

from laces.typing import Renderable


register = template.library.Library()
Expand All @@ -16,19 +27,19 @@ class ComponentNode(template.Node):

def __init__(
self,
component,
extra_context=None,
isolated_context=False,
fallback_render_method=None,
target_var=None,
):
component: "FilterExpression",
extra_context: "Optional[dict[str, FilterExpression]]" = None,
isolated_context: bool = False,
fallback_render_method: "Optional[FilterExpression]" = None,
target_var: "Optional[str]" = None,
) -> None:
self.component = component
self.extra_context = extra_context or {}
self.isolated_context = isolated_context
self.fallback_render_method = fallback_render_method
self.target_var = target_var

def render(self, context: template.Context) -> str:
def render(self, context: template.Context) -> SafeString:
"""
Render the ComponentNode template node.

Expand All @@ -51,7 +62,7 @@ def render(self, context: template.Context) -> str:
The `as` keyword can be used to store the rendered component in a variable
in the parent context. The variable name is passed after the `as` keyword.
"""
component = self.component.resolve(context)
component: "Renderable" = self.component.resolve(context)

if self.fallback_render_method:
fallback_render_method = self.fallback_render_method.resolve(context)
Expand All @@ -75,15 +86,15 @@ def render(self, context: template.Context) -> str:

if self.target_var:
context[self.target_var] = html
return ""
return SafeString("")
else:
if context.autoescape:
html = conditional_escape(html)
return html


@register.tag(name="component")
def component(parser, token):
def component(parser: "Parser", token: "Token") -> ComponentNode:
"""
Template tag to render a component via ComponentNode.

Expand Down
65 changes: 52 additions & 13 deletions laces/test/example/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,68 @@
"""

from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING

from django.utils.html import format_html

from laces.components import Component


if TYPE_CHECKING:
from typing import Any, Dict, Optional

from django.utils.safestring import SafeString

from laces.typing import RenderContext


class RendersTemplateWithFixedContentComponent(Component):
template_name = "components/hello-world.html"


class ReturnsFixedContentComponent(Component):
def render_html(self, parent_context=None):
def render_html(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "SafeString":
return format_html("<h1>Hello World Return</h1>\n")


class PassesFixedNameToContextComponent(Component):
template_name = "components/hello-name.html"

def get_context_data(self, parent_context=None):
def get_context_data(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "RenderContext":
return {"name": "Alice"}


class PassesInstanceAttributeToContextComponent(Component):
template_name = "components/hello-name.html"

def __init__(self, name, **kwargs):
def __init__(self, name: str, **kwargs: "Dict[str, Any]") -> None:
super().__init__(**kwargs)
self.name = name

def get_context_data(self, parent_context=None):
def get_context_data(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "RenderContext":
return {"name": self.name}


class PassesSelfToContextComponent(Component):
template_name = "components/hello-self-name.html"

def __init__(self, name, **kwargs):
def __init__(self, name: str, **kwargs: "Dict[str, Any]") -> None:
super().__init__(**kwargs)
self.name = name

def get_context_data(self, parent_context=None):
def get_context_data(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "RenderContext":
return {"self": self}


Expand All @@ -56,14 +77,17 @@ class DataclassAsDictContextComponent(Component):

name: str

def get_context_data(self, parent_context=None):
def get_context_data(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "RenderContext":
return asdict(self)


class PassesNameFromParentContextComponent(Component):
template_name = "components/hello-name.html"

def get_context_data(self, parent_context):
def get_context_data(self, parent_context: "RenderContext") -> "RenderContext":
return {"name": parent_context["name"]}


Expand All @@ -75,7 +99,10 @@ def __init__(self, heading: "HeadingComponent", content: "ParagraphComponent"):
self.heading = heading
self.content = content

def get_context_data(self, parent_context=None):
def get_context_data(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "RenderContext":
return {
"heading": self.heading,
"content": self.content,
Expand All @@ -87,7 +114,10 @@ def __init__(self, text: str):
super().__init__()
self.text = text

def render_html(self, parent_context=None):
def render_html(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "SafeString":
return format_html("<h2>{}</h2>\n", self.text)


Expand All @@ -96,7 +126,10 @@ def __init__(self, text: str):
super().__init__()
self.text = text

def render_html(self, parent_context=None):
def render_html(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "SafeString":
return format_html("<p>{}</p>\n", self.text)


Expand All @@ -108,7 +141,10 @@ def __init__(self, heading: "HeadingComponent", items: "list[Component]"):
self.heading = heading
self.items = items

def get_context_data(self, parent_context=None):
def get_context_data(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "RenderContext":
return {
"heading": self.heading,
"items": self.items,
Expand All @@ -120,5 +156,8 @@ def __init__(self, text: str):
super().__init__()
self.text = text

def render_html(self, parent_context=None):
def render_html(
self,
parent_context: "Optional[RenderContext]" = None,
) -> "SafeString":
return format_html("<blockquote>{}</blockquote>\n", self.text)
8 changes: 7 additions & 1 deletion laces/test/example/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import TYPE_CHECKING

from django.shortcuts import render

from laces.test.example.components import (
Expand All @@ -16,7 +18,11 @@
)


def kitchen_sink(request):
if TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse


def kitchen_sink(request: "HttpRequest") -> "HttpResponse":
"""Render a page with all example components."""
fixed_content_template = RendersTemplateWithFixedContentComponent()
fixed_content_return = ReturnsFixedContentComponent()
Expand Down
Loading
Loading