mirror of
https://github.com/django-components/django-components.git
synced 2025-12-09 22:11:06 +00:00
Add mypy support (#396), thanks @JuroOravec
This commit is contained in:
parent
4090c928ee
commit
b14dec9777
14 changed files with 104 additions and 58 deletions
|
|
@ -37,6 +37,8 @@ def import_file(path):
|
||||||
MODULE_PATH = path
|
MODULE_PATH = path
|
||||||
MODULE_NAME = Path(path).stem
|
MODULE_NAME = Path(path).stem
|
||||||
spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
|
spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
|
||||||
|
if spec is None:
|
||||||
|
raise ValueError(f"Cannot import file '{path}' - invalid path")
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
sys.modules[spec.name] = module
|
sys.modules[spec.name] = module
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module) # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import difflib
|
import difflib
|
||||||
import inspect
|
import inspect
|
||||||
from collections import ChainMap
|
from collections import ChainMap
|
||||||
from typing import Any, ClassVar, Dict, Iterable, Optional, Set, Tuple, Union
|
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.forms.widgets import Media, MediaDefiningClass
|
from django.forms.widgets import Media, MediaDefiningClass
|
||||||
|
|
@ -40,7 +40,7 @@ from django_components.templatetags.component_tags import (
|
||||||
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
def __new__(mcs, name, bases, attrs):
|
def __new__(mcs, name, bases, attrs):
|
||||||
if "Media" in attrs:
|
if "Media" in attrs:
|
||||||
media = attrs["Media"]
|
media: Component.Media = attrs["Media"]
|
||||||
|
|
||||||
# Allow: class Media: css = "style.css"
|
# Allow: class Media: css = "style.css"
|
||||||
if hasattr(media, "css") and isinstance(media.css, str):
|
if hasattr(media, "css") and isinstance(media.css, str):
|
||||||
|
|
@ -54,7 +54,7 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
if hasattr(media, "css") and isinstance(media.css, dict):
|
if hasattr(media, "css") and isinstance(media.css, dict):
|
||||||
for media_type, path_list in media.css.items():
|
for media_type, path_list in media.css.items():
|
||||||
if isinstance(path_list, str):
|
if isinstance(path_list, str):
|
||||||
media.css[media_type] = [path_list]
|
media.css[media_type] = [path_list] # type: ignore
|
||||||
|
|
||||||
# Allow: class Media: js = "script.js"
|
# Allow: class Media: js = "script.js"
|
||||||
if hasattr(media, "js") and isinstance(media.js, str):
|
if hasattr(media, "js") and isinstance(media.js, str):
|
||||||
|
|
@ -67,20 +67,20 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
# Either template_name or template must be set on subclass OR subclass must implement get_template() with
|
# Either template_name or template must be set on subclass OR subclass must implement get_template() with
|
||||||
# non-null return.
|
# non-null return.
|
||||||
template_name: ClassVar[Optional[str]] = None
|
template_name: ClassVar[Optional[str]] = None
|
||||||
template: ClassVar[Optional[str]] = None
|
template: Optional[str] = None
|
||||||
js: ClassVar[Optional[str]] = None
|
js: Optional[str] = None
|
||||||
css: ClassVar[Optional[str]] = None
|
css: Optional[str] = None
|
||||||
media: Media
|
media: Media
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {}
|
css: Optional[Union[str, List[str], Dict[str, str], Dict[str, List[str]]]] = None
|
||||||
js = []
|
js: Optional[Union[str, List[str]]] = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
registered_name: Optional[str] = None,
|
registered_name: Optional[str] = None,
|
||||||
outer_context: Optional[Context] = None,
|
outer_context: Optional[Context] = None,
|
||||||
fill_content: Union[DefaultFillContent, Iterable[NamedFillContent]] = (),
|
fill_content: Union[DefaultFillContent, Iterable[NamedFillContent]] = (), # type: ignore
|
||||||
):
|
):
|
||||||
self.registered_name: Optional[str] = registered_name
|
self.registered_name: Optional[str] = registered_name
|
||||||
self.outer_context: Context = outer_context or Context()
|
self.outer_context: Context = outer_context or Context()
|
||||||
|
|
@ -142,7 +142,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
self,
|
self,
|
||||||
context_data: Dict[str, Any],
|
context_data: Dict[str, Any],
|
||||||
slots_data: Optional[Dict[SlotName, str]] = None,
|
slots_data: Optional[Dict[SlotName, str]] = None,
|
||||||
escape_slots_content: Optional[bool] = True,
|
escape_slots_content: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
context = Context(context_data)
|
context = Context(context_data)
|
||||||
template = self.get_template(context)
|
template = self.get_template(context)
|
||||||
|
|
@ -160,7 +160,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
self,
|
self,
|
||||||
context_data: Dict[str, Any],
|
context_data: Dict[str, Any],
|
||||||
slots_data: Optional[Dict[SlotName, str]] = None,
|
slots_data: Optional[Dict[SlotName, str]] = None,
|
||||||
escape_slots_content: Optional[bool] = True,
|
escape_slots_content: bool = True,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
|
@ -173,7 +173,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
def _fill_slots(
|
def _fill_slots(
|
||||||
self,
|
self,
|
||||||
slots_data: Dict[SlotName, str],
|
slots_data: Dict[SlotName, str],
|
||||||
escape_content: bool,
|
escape_content: bool = True,
|
||||||
):
|
):
|
||||||
"""Fill component slots outside of template rendering."""
|
"""Fill component slots outside of template rendering."""
|
||||||
self.fill_content = [
|
self.fill_content = [
|
||||||
|
|
@ -195,7 +195,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
named_fills_content = {}
|
named_fills_content = {}
|
||||||
else:
|
else:
|
||||||
default_fill_content = None
|
default_fill_content = None
|
||||||
named_fills_content = {name: (nodelist, alias) for name, nodelist, alias in self.fill_content}
|
named_fills_content = {name: (nodelist, alias) for name, nodelist, alias in list(self.fill_content)}
|
||||||
|
|
||||||
# If value is `None`, then slot is unfilled.
|
# If value is `None`, then slot is unfilled.
|
||||||
slot_name2fill_content: Dict[SlotName, Optional[FillContent]] = {}
|
slot_name2fill_content: Dict[SlotName, Optional[FillContent]] = {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Type, Union
|
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 9):
|
if sys.version_info[:2] < (3, 9):
|
||||||
from typing import ChainMap
|
from typing import ChainMap
|
||||||
|
|
@ -194,7 +194,7 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
||||||
|
|
||||||
extra_context = {}
|
extra_context = {}
|
||||||
try:
|
try:
|
||||||
slot_fill_content: Optional[FillContent] = filled_slots_map[(self.name, self.template)]
|
slot_fill_content: FillContent = filled_slots_map[(self.name, self.template)]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if self.is_required:
|
if self.is_required:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
|
|
@ -375,16 +375,17 @@ class ComponentNode(Node):
|
||||||
# Note that outer component context is used to resolve variables in
|
# Note that outer component context is used to resolve variables in
|
||||||
# fill tag.
|
# fill tag.
|
||||||
resolved_name = fill_node.name_fexp.resolve(context)
|
resolved_name = fill_node.name_fexp.resolve(context)
|
||||||
|
resolved_alias: Optional[str]
|
||||||
if fill_node.alias_fexp:
|
if fill_node.alias_fexp:
|
||||||
resolved_alias: str = fill_node.alias_fexp.resolve(context)
|
resolved_alias = fill_node.alias_fexp.resolve(context)
|
||||||
if not resolved_alias.isidentifier():
|
if resolved_alias and not resolved_alias.isidentifier():
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Fill tag alias '{fill_node.alias_fexp.var}' in component "
|
f"Fill tag alias '{fill_node.alias_fexp.var}' in component "
|
||||||
f"{resolved_component_name} does not resolve to "
|
f"{resolved_component_name} does not resolve to "
|
||||||
f"a valid Python identifier. Got: '{resolved_alias}'."
|
f"a valid Python identifier. Got: '{resolved_alias}'."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
resolved_alias: None = None
|
resolved_alias = None
|
||||||
fill_content.append((resolved_name, fill_node.nodelist, resolved_alias))
|
fill_content.append((resolved_name, fill_node.nodelist, resolved_alias))
|
||||||
|
|
||||||
component: Component = component_cls(
|
component: Component = component_cls(
|
||||||
|
|
@ -426,14 +427,15 @@ def do_component(parser, token):
|
||||||
component_name, context_args, context_kwargs = parse_component_with_args(parser, bits, "component")
|
component_name, context_args, context_kwargs = parse_component_with_args(parser, bits, "component")
|
||||||
body: NodeList = parser.parse(parse_until=["endcomponent"])
|
body: NodeList = parser.parse(parse_until=["endcomponent"])
|
||||||
parser.delete_first_token()
|
parser.delete_first_token()
|
||||||
fill_nodes = ()
|
fill_nodes: Union[Iterable[NamedFillNode], ImplicitFillNode] = []
|
||||||
if block_has_content(body):
|
if block_has_content(body):
|
||||||
for parse_fn in (
|
for parse_fn in (
|
||||||
try_parse_as_default_fill,
|
try_parse_as_default_fill,
|
||||||
try_parse_as_named_fill_tag_set,
|
try_parse_as_named_fill_tag_set,
|
||||||
):
|
):
|
||||||
fill_nodes = parse_fn(body)
|
curr_fill_nodes = parse_fn(body)
|
||||||
if fill_nodes:
|
if curr_fill_nodes:
|
||||||
|
fill_nodes = curr_fill_nodes
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
|
|
@ -457,7 +459,7 @@ def try_parse_as_named_fill_tag_set(
|
||||||
nodelist: NodeList,
|
nodelist: NodeList,
|
||||||
) -> Optional[Iterable[NamedFillNode]]:
|
) -> Optional[Iterable[NamedFillNode]]:
|
||||||
result = []
|
result = []
|
||||||
seen_name_fexps = set()
|
seen_name_fexps: Set[FilterExpression] = set()
|
||||||
for node in nodelist:
|
for node in nodelist:
|
||||||
if isinstance(node, NamedFillNode):
|
if isinstance(node, NamedFillNode):
|
||||||
if node.name_fexp in seen_name_fexps:
|
if node.name_fexp in seen_name_fexps:
|
||||||
|
|
@ -465,6 +467,7 @@ def try_parse_as_named_fill_tag_set(
|
||||||
f"Multiple fill tags cannot target the same slot name: "
|
f"Multiple fill tags cannot target the same slot name: "
|
||||||
f"Detected duplicate fill tag name '{node.name_fexp}'."
|
f"Detected duplicate fill tag name '{node.name_fexp}'."
|
||||||
)
|
)
|
||||||
|
seen_name_fexps.add(node.name_fexp)
|
||||||
result.append(node)
|
result.append(node)
|
||||||
elif isinstance(node, CommentNode):
|
elif isinstance(node, CommentNode):
|
||||||
pass
|
pass
|
||||||
|
|
@ -544,9 +547,13 @@ def do_if_filled_block(parser, token):
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
starting_tag = bits[0]
|
starting_tag = bits[0]
|
||||||
slot_name, is_positive = parse_if_filled_bits(bits)
|
slot_name, is_positive = parse_if_filled_bits(bits)
|
||||||
nodelist = parser.parse(("elif_filled", "else_filled", "endif_filled"))
|
nodelist: NodeList = parser.parse(("elif_filled", "else_filled", "endif_filled"))
|
||||||
branches: List[_IfSlotFilledBranchNode] = [
|
branches: List[_IfSlotFilledBranchNode] = [
|
||||||
IfSlotFilledConditionBranchNode(slot_name=slot_name, nodelist=nodelist, is_positive=is_positive)
|
IfSlotFilledConditionBranchNode(
|
||||||
|
slot_name=slot_name, # type: ignore
|
||||||
|
nodelist=nodelist,
|
||||||
|
is_positive=is_positive,
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
token = parser.next_token()
|
token = parser.next_token()
|
||||||
|
|
@ -555,9 +562,13 @@ def do_if_filled_block(parser, token):
|
||||||
while token.contents.startswith("elif_filled"):
|
while token.contents.startswith("elif_filled"):
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
slot_name, is_positive = parse_if_filled_bits(bits)
|
slot_name, is_positive = parse_if_filled_bits(bits)
|
||||||
nodelist: NodeList = parser.parse(("elif_filled", "else_filled", "endif_filled"))
|
nodelist = parser.parse(("elif_filled", "else_filled", "endif_filled"))
|
||||||
branches.append(
|
branches.append(
|
||||||
IfSlotFilledConditionBranchNode(slot_name=slot_name, nodelist=nodelist, is_positive=is_positive)
|
IfSlotFilledConditionBranchNode(
|
||||||
|
slot_name=slot_name, # type: ignore
|
||||||
|
nodelist=nodelist,
|
||||||
|
is_positive=is_positive,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
token = parser.next_token()
|
token = parser.next_token()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
try:
|
try:
|
||||||
from typing import Annotated
|
from typing import Annotated # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
||||||
class Annotated:
|
class Annotated: # type: ignore
|
||||||
def __init__(self, type_, *args, **kwargs):
|
def __init__(self, type_, *args, **kwargs):
|
||||||
self.type_ = type_
|
self.type_ = type_
|
||||||
self.metadata = args, kwargs
|
self.metadata = args, kwargs
|
||||||
|
|
@ -13,7 +13,7 @@ except ImportError:
|
||||||
def __getitem__(self, params):
|
def __getitem__(self, params):
|
||||||
if not isinstance(params, tuple):
|
if not isinstance(params, tuple):
|
||||||
params = (params,)
|
params = (params,)
|
||||||
return Annotated(self.type_, *params, **self.metadata[1])
|
return Annotated(self.type_, *params, **self.metadata[1]) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
css = Annotated[str, "css"]
|
css = Annotated[str, "css"]
|
||||||
|
|
|
||||||
0
py.typed
Normal file
0
py.typed
Normal file
|
|
@ -35,4 +35,11 @@ exclude = [
|
||||||
'.env',
|
'.env',
|
||||||
'.venv',
|
'.venv',
|
||||||
'.tox',
|
'.tox',
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
check_untyped_defs = true
|
||||||
|
ignore_missing_imports = true
|
||||||
|
exclude = [
|
||||||
|
'test_structures'
|
||||||
]
|
]
|
||||||
|
|
@ -5,4 +5,5 @@ flake8
|
||||||
flake8-pyproject
|
flake8-pyproject
|
||||||
isort
|
isort
|
||||||
pre-commit
|
pre-commit
|
||||||
black
|
black
|
||||||
|
mypy
|
||||||
|
|
@ -40,8 +40,12 @@ isort==5.13.2
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
mccabe==0.7.0
|
mccabe==0.7.0
|
||||||
# via flake8
|
# via flake8
|
||||||
|
mypy==1.9.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
# via black
|
# via
|
||||||
|
# black
|
||||||
|
# mypy
|
||||||
nodeenv==1.8.0
|
nodeenv==1.8.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
packaging==23.2
|
packaging==23.2
|
||||||
|
|
@ -77,6 +81,8 @@ sqlparse==0.4.4
|
||||||
# via django
|
# via django
|
||||||
tox==4.14.1
|
tox==4.14.1
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
|
typing-extensions==4.10.0
|
||||||
|
# via mypy
|
||||||
virtualenv==20.25.0
|
virtualenv==20.25.0
|
||||||
# via
|
# via
|
||||||
# pre-commit
|
# pre-commit
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
@ -16,7 +17,7 @@ SECRET_KEY = os.environ.get("SECRET_KEY", secrets.token_hex(100))
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
|
Version = Tuple[int, ...]
|
||||||
|
VersionMapping = Dict[Version, List[Version]]
|
||||||
|
|
||||||
def get_supported_versions(url):
|
|
||||||
|
def get_supported_versions(url: str):
|
||||||
with request.urlopen(url) as response:
|
with request.urlopen(url) as response:
|
||||||
response_content = response.read()
|
response_content = response.read()
|
||||||
|
|
||||||
|
|
@ -39,7 +43,7 @@ def get_supported_versions(url):
|
||||||
return parse_supported_versions(content)
|
return parse_supported_versions(content)
|
||||||
|
|
||||||
|
|
||||||
def get_latest_version(url):
|
def get_latest_version(url: str):
|
||||||
with request.urlopen(url) as response:
|
with request.urlopen(url) as response:
|
||||||
response_content = response.read()
|
response_content = response.read()
|
||||||
|
|
||||||
|
|
@ -48,12 +52,12 @@ def get_latest_version(url):
|
||||||
return version_to_tuple(version_string)
|
return version_to_tuple(version_string)
|
||||||
|
|
||||||
|
|
||||||
def version_to_tuple(version_string):
|
def version_to_tuple(version_string: str):
|
||||||
return tuple(int(num) for num in version_string.split("."))
|
return tuple(int(num) for num in version_string.split("."))
|
||||||
|
|
||||||
|
|
||||||
def build_python_to_django(django_to_python, latest_version):
|
def build_python_to_django(django_to_python: VersionMapping, latest_version: Version):
|
||||||
python_to_django = defaultdict(list)
|
python_to_django: VersionMapping = defaultdict(list)
|
||||||
for django_version, python_versions in django_to_python.items():
|
for django_version, python_versions in django_to_python.items():
|
||||||
for python_version in python_versions:
|
for python_version in python_versions:
|
||||||
if django_version <= latest_version:
|
if django_version <= latest_version:
|
||||||
|
|
@ -67,21 +71,21 @@ def env_format(version_tuple, divider=""):
|
||||||
return divider.join(str(num) for num in version_tuple)
|
return divider.join(str(num) for num in version_tuple)
|
||||||
|
|
||||||
|
|
||||||
def build_tox_envlist(python_to_django):
|
def build_tox_envlist(python_to_django: VersionMapping):
|
||||||
lines = [
|
lines_data = [
|
||||||
(
|
(
|
||||||
env_format(python_version),
|
env_format(python_version),
|
||||||
",".join(env_format(version) for version in django_versions),
|
",".join(env_format(version) for version in django_versions),
|
||||||
)
|
)
|
||||||
for python_version, django_versions in python_to_django.items()
|
for python_version, django_versions in python_to_django.items()
|
||||||
]
|
]
|
||||||
lines = [f"py{a}-django{{{b}}}" for a, b in lines]
|
lines = [f"py{a}-django{{{b}}}" for a, b in lines_data]
|
||||||
version_lines = "\n".join([version for version in lines])
|
version_lines = "\n".join([version for version in lines])
|
||||||
return "envlist = \n" + textwrap.indent(version_lines, prefix=" ")
|
return "envlist = \n" + textwrap.indent(version_lines, prefix=" ")
|
||||||
|
|
||||||
|
|
||||||
def build_gh_actions_envlist(python_to_django):
|
def build_gh_actions_envlist(python_to_django: VersionMapping):
|
||||||
lines = [
|
lines_data = [
|
||||||
(
|
(
|
||||||
env_format(python_version, divider="."),
|
env_format(python_version, divider="."),
|
||||||
env_format(python_version),
|
env_format(python_version),
|
||||||
|
|
@ -89,18 +93,18 @@ def build_gh_actions_envlist(python_to_django):
|
||||||
)
|
)
|
||||||
for python_version, django_versions in python_to_django.items()
|
for python_version, django_versions in python_to_django.items()
|
||||||
]
|
]
|
||||||
lines = [f"{a}: py{b}-django{{{c}}}" for a, b, c in lines]
|
lines = [f"{a}: py{b}-django{{{c}}}" for a, b, c in lines_data]
|
||||||
version_lines = "\n".join([version for version in lines])
|
version_lines = "\n".join([version for version in lines])
|
||||||
return "python = \n" + textwrap.indent(version_lines, prefix=" ")
|
return "python = \n" + textwrap.indent(version_lines, prefix=" ")
|
||||||
|
|
||||||
|
|
||||||
def build_deps_envlist(python_to_django):
|
def build_deps_envlist(python_to_django: VersionMapping):
|
||||||
all_django_versions = set()
|
all_django_versions = set()
|
||||||
for django_versions in python_to_django.values():
|
for django_versions in python_to_django.values():
|
||||||
for django_version in django_versions:
|
for django_version in django_versions:
|
||||||
all_django_versions.add(django_version)
|
all_django_versions.add(django_version)
|
||||||
|
|
||||||
lines = [
|
lines_data = [
|
||||||
(
|
(
|
||||||
env_format(django_version),
|
env_format(django_version),
|
||||||
env_format(django_version, divider="."),
|
env_format(django_version, divider="."),
|
||||||
|
|
@ -108,11 +112,11 @@ def build_deps_envlist(python_to_django):
|
||||||
)
|
)
|
||||||
for django_version in sorted(all_django_versions)
|
for django_version in sorted(all_django_versions)
|
||||||
]
|
]
|
||||||
lines = [f"django{a}: Django>={b},<{c}" for a, b, c in sorted(lines)]
|
lines = [f"django{a}: Django>={b},<{c}" for a, b, c in sorted(lines_data)]
|
||||||
return "deps = \n" + textwrap.indent("\n".join(lines), prefix=" ")
|
return "deps = \n" + textwrap.indent("\n".join(lines), prefix=" ")
|
||||||
|
|
||||||
|
|
||||||
def build_pypi_classifiers(python_to_django):
|
def build_pypi_classifiers(python_to_django: VersionMapping):
|
||||||
classifiers = []
|
classifiers = []
|
||||||
|
|
||||||
all_python_versions = python_to_django.keys()
|
all_python_versions = python_to_django.keys()
|
||||||
|
|
@ -130,7 +134,7 @@ def build_pypi_classifiers(python_to_django):
|
||||||
return textwrap.indent("classifiers=[\n", prefix=" " * 4) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
|
return textwrap.indent("classifiers=[\n", prefix=" " * 4) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
|
||||||
|
|
||||||
|
|
||||||
def build_readme(python_to_django):
|
def build_readme(python_to_django: VersionMapping):
|
||||||
print(
|
print(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
"""\
|
"""\
|
||||||
|
|
@ -139,19 +143,19 @@ def build_readme(python_to_django):
|
||||||
""".rstrip()
|
""".rstrip()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
lines = [
|
lines_data = [
|
||||||
(
|
(
|
||||||
env_format(python_version, divider="."),
|
env_format(python_version, divider="."),
|
||||||
", ".join(env_format(version, divider=".") for version in django_versions),
|
", ".join(env_format(version, divider=".") for version in django_versions),
|
||||||
)
|
)
|
||||||
for python_version, django_versions in python_to_django.items()
|
for python_version, django_versions in python_to_django.items()
|
||||||
]
|
]
|
||||||
lines = [f"| {a: <14} | {b: <24} |" for a, b in lines]
|
lines = [f"| {a: <14} | {b: <24} |" for a, b in lines_data]
|
||||||
version_lines = "\n".join([version for version in lines])
|
version_lines = "\n".join([version for version in lines])
|
||||||
return version_lines
|
return version_lines
|
||||||
|
|
||||||
|
|
||||||
def build_pyenv(python_to_django):
|
def build_pyenv(python_to_django: VersionMapping):
|
||||||
lines = []
|
lines = []
|
||||||
all_python_versions = python_to_django.keys()
|
all_python_versions = python_to_django.keys()
|
||||||
for python_version in all_python_versions:
|
for python_version in all_python_versions:
|
||||||
|
|
@ -164,13 +168,13 @@ def build_pyenv(python_to_django):
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def build_ci_python_versions(python_to_django):
|
def build_ci_python_versions(python_to_django: Dict[str, str]):
|
||||||
# Outputs python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
|
# Outputs python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
|
||||||
lines = [
|
lines = [
|
||||||
f"'{env_format(python_version, divider='.')}'" for python_version, django_versions in python_to_django.items()
|
f"'{env_format(python_version, divider='.')}'" for python_version, django_versions in python_to_django.items()
|
||||||
]
|
]
|
||||||
lines = " " * 8 + f"python-version: [{', '.join(lines)}]"
|
lines_formatted = " " * 8 + f"python-version: [{', '.join(lines)}]"
|
||||||
return lines
|
return lines_formatted
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
3
setup.py
3
setup.py
|
|
@ -8,6 +8,9 @@ VERSION = "0.51"
|
||||||
setup(
|
setup(
|
||||||
name="django_components",
|
name="django_components",
|
||||||
packages=find_packages(exclude=["tests"]),
|
packages=find_packages(exclude=["tests"]),
|
||||||
|
package_data={
|
||||||
|
"django_components": ["py.typed"],
|
||||||
|
},
|
||||||
version=VERSION,
|
version=VERSION,
|
||||||
description="A way to create simple reusable template components in Django.",
|
description="A way to create simple reusable template components in Django.",
|
||||||
long_description=open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8").read(),
|
long_description=open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8").read(),
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ class VariableDisplay(component.Component):
|
||||||
class IncrementerComponent(component.Component):
|
class IncrementerComponent(component.Component):
|
||||||
template_name = "incrementer.html"
|
template_name = "incrementer.html"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.call_count = 0
|
||||||
|
|
||||||
def get_context_data(self, value=0):
|
def get_context_data(self, value=0):
|
||||||
value = int(value)
|
value = int(value)
|
||||||
if hasattr(self, "call_count"):
|
if hasattr(self, "call_count"):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Callable, Iterable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
|
||||||
|
|
@ -1250,7 +1250,7 @@ class IterationFillTest(SimpleTestCase):
|
||||||
class ComponentSimpleSlotInALoop(django_components.component.Component):
|
class ComponentSimpleSlotInALoop(django_components.component.Component):
|
||||||
template_name = "template_with_slot_in_a_loop.html"
|
template_name = "template_with_slot_in_a_loop.html"
|
||||||
|
|
||||||
def get_context_data(self, objects: Iterable) -> dict:
|
def get_context_data(self, objects, *args, **kwargs) -> dict:
|
||||||
return {
|
return {
|
||||||
"objects": objects,
|
"objects": objects,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
tox.ini
9
tox.ini
|
|
@ -13,6 +13,7 @@ envlist =
|
||||||
flake8
|
flake8
|
||||||
isort
|
isort
|
||||||
coverage
|
coverage
|
||||||
|
mypy
|
||||||
requires = virtualenv<20.22.0
|
requires = virtualenv<20.22.0
|
||||||
|
|
||||||
# https://tox.wiki/en/latest/faq.html#testing-end-of-life-python-versions
|
# https://tox.wiki/en/latest/faq.html#testing-end-of-life-python-versions
|
||||||
|
|
@ -25,7 +26,7 @@ python =
|
||||||
3.9: py39-django{32,40,41,42}
|
3.9: py39-django{32,40,41,42}
|
||||||
3.10: py310-django{32,40,41,42,50}
|
3.10: py310-django{32,40,41,42,50}
|
||||||
3.11: py311-django{41,42,50}
|
3.11: py311-django{41,42,50}
|
||||||
3.12: py312-django{42,50}, flake8, isort, coverage
|
3.12: py312-django{42,50}, flake8, isort, coverage, mypy
|
||||||
fail_on_no_env = True
|
fail_on_no_env = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
|
@ -65,3 +66,9 @@ deps = pytest-coverage
|
||||||
commands =
|
commands =
|
||||||
coverage run --branch -m pytest
|
coverage run --branch -m pytest
|
||||||
coverage report -m --fail-under=93
|
coverage report -m --fail-under=93
|
||||||
|
|
||||||
|
[testenv:mypy]
|
||||||
|
changedir = {toxinidir}
|
||||||
|
deps = mypy
|
||||||
|
commands =
|
||||||
|
mypy .
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue