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 typing support to get_solo #128

Merged
merged 10 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
include *.md
include LICENSE
include CHANGES
include solo/py.typed
Viicos marked this conversation as resolved.
Show resolved Hide resolved
recursive-include solo/templates *
recursive-include solo/locale *.mo *.po
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[tool.mypy]
ignore_missing_imports = true
strict = true
exclude = "solo/tests"
31 changes: 18 additions & 13 deletions solo/admin.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
from django.urls import re_path
from __future__ import annotations

from typing import Any

from django.db.models import Model
from django.urls import URLPattern, re_path
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.utils.encoding import force_str
from django.utils.translation import gettext as _

from solo.models import DEFAULT_SINGLETON_INSTANCE_ID
from solo import settings as solo_settings


class SingletonModelAdmin(admin.ModelAdmin):
class SingletonModelAdmin(admin.ModelAdmin): # type: ignore[type-arg]
johnthagen marked this conversation as resolved.
Show resolved Hide resolved
object_history_template = "admin/solo/object_history.html"
change_form_template = "admin/solo/change_form.html"

def has_add_permission(self, request):
def has_add_permission(self, request: HttpRequest) -> bool:
return False

def has_delete_permission(self, request, obj=None):
def has_delete_permission(self, request: HttpRequest, obj: Model | None = None) -> bool:
return False

def get_urls(self):
urls = super(SingletonModelAdmin, self).get_urls()
def get_urls(self) -> list[URLPattern]:
urls = super().get_urls()

if not solo_settings.SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE:
return urls
Expand Down Expand Up @@ -50,7 +55,7 @@ def get_urls(self):
# By inserting the custom URLs first, we overwrite the standard URLs.
return custom_urls + urls

def response_change(self, request, obj):
def response_change(self, request: HttpRequest, obj: Model) -> HttpResponseRedirect:
msg = _('%(obj)s was changed successfully.') % {
'obj': force_str(obj)}
if '_continue' in request.POST:
Expand All @@ -61,32 +66,32 @@ def response_change(self, request, obj):
self.message_user(request, msg)
return HttpResponseRedirect("../../")

def change_view(self, request, object_id, form_url='', extra_context=None):
def change_view(self, request: HttpRequest, object_id: str, form_url: str = '', extra_context: dict[str, Any] | None = None) -> HttpResponse:
if object_id == str(self.singleton_instance_id):
self.model.objects.get_or_create(pk=self.singleton_instance_id)

if not extra_context:
extra_context = dict()
extra_context['skip_object_list_page'] = solo_settings.SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE

return super(SingletonModelAdmin, self).change_view(
return super().change_view(
request,
object_id,
form_url=form_url,
extra_context=extra_context,
)

def history_view(self, request, object_id, extra_context=None):
def history_view(self, request: HttpRequest, object_id: str, extra_context: dict[str, Any] | None = None) -> HttpResponse:
if not extra_context:
extra_context = dict()
extra_context['skip_object_list_page'] = solo_settings.SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE

return super(SingletonModelAdmin, self).history_view(
return super().history_view(
request,
object_id,
extra_context=extra_context,
)

@property
def singleton_instance_id(self):
def singleton_instance_id(self) -> int:
return getattr(self.model, 'singleton_instance_id', DEFAULT_SINGLETON_INSTANCE_ID)
44 changes: 23 additions & 21 deletions solo/models.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,65 @@
from __future__ import annotations

from typing import Any, TypeVar

from django.conf import settings
from django.core.cache import caches
from django.db import models

try:
from django.core.cache import caches
get_cache = lambda cache_name: caches[cache_name]
except ImportError:
from django.core.cache import get_cache

from solo import settings as solo_settings

DEFAULT_SINGLETON_INSTANCE_ID = 1

Self = TypeVar("Self", bound="SingletonModel")
johnthagen marked this conversation as resolved.
Show resolved Hide resolved


class SingletonModel(models.Model):
singleton_instance_id = DEFAULT_SINGLETON_INSTANCE_ID

class Meta:
abstract = True

def save(self, *args, **kwargs):
def save(self, *args: Any, **kwargs: Any) -> None:
self.pk = self.singleton_instance_id
super(SingletonModel, self).save(*args, **kwargs)
super().save(*args, **kwargs)
self.set_to_cache()

def delete(self, *args, **kwargs):
def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]:
self.clear_cache()
super(SingletonModel, self).delete(*args, **kwargs)
return super().delete(*args, **kwargs)
johnthagen marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def clear_cache(cls):
def clear_cache(cls) -> None:
cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE)
if cache_name:
cache = get_cache(cache_name)
cache = caches[cache_name]
cache_key = cls.get_cache_key()
cache.delete(cache_key)

def set_to_cache(self):
def set_to_cache(self) -> None:
cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE)
if not cache_name:
return None
cache = get_cache(cache_name)
cache = caches[cache_name]
cache_key = self.get_cache_key()
timeout = getattr(settings, 'SOLO_CACHE_TIMEOUT', solo_settings.SOLO_CACHE_TIMEOUT)
cache.set(cache_key, self, timeout)

@classmethod
def get_cache_key(cls):
def get_cache_key(cls) -> str:
prefix = getattr(settings, 'SOLO_CACHE_PREFIX', solo_settings.SOLO_CACHE_PREFIX)
return '%s:%s' % (prefix, cls.__name__.lower())

@classmethod
def get_solo(cls):
def get_solo(cls: type[Self]) -> Self:
cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE)
if not cache_name:
obj, created = cls.objects.get_or_create(pk=cls.singleton_instance_id)
return obj
cache = get_cache(cache_name)
obj, _ = cls.objects.get_or_create(pk=cls.singleton_instance_id)
return obj # type: ignore[return-value]
johnthagen marked this conversation as resolved.
Show resolved Hide resolved
cache = caches[cache_name]
cache_key = cls.get_cache_key()
obj = cache.get(cache_key)
if not obj:
obj, created = cls.objects.get_or_create(pk=cls.singleton_instance_id)
obj, _ = cls.objects.get_or_create(pk=cls.singleton_instance_id)
obj.set_to_cache()
return obj
return obj # type: ignore[return-value]
johnthagen marked this conversation as resolved.
Show resolved Hide resolved
Empty file added solo/py.typed
Empty file.
16 changes: 10 additions & 6 deletions solo/settings.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
from __future__ import annotations

from django.conf import settings

# template parameters
GET_SOLO_TEMPLATE_TAG_NAME = getattr(settings,
'GET_SOLO_TEMPLATE_TAG_NAME', 'get_solo')
GET_SOLO_TEMPLATE_TAG_NAME: str = getattr(
settings, 'GET_SOLO_TEMPLATE_TAG_NAME', 'get_solo'
)

SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE = getattr(settings,
'SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE', True)
SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE: bool = getattr(
settings, 'SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE', True
)

# The cache that should be used, e.g. 'default'. Refers to Django CACHES setting.
# Set to None to disable caching.
SOLO_CACHE = None
SOLO_CACHE: str | None = None

SOLO_CACHE_TIMEOUT = 60*5
SOLO_CACHE_TIMEOUT = 60 * 5

SOLO_CACHE_PREFIX = 'solo'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
13 changes: 4 additions & 9 deletions solo/templatetags/solo_tags.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
from django import template
from django.apps import apps
from django.utils.translation import gettext as _

from solo import settings as solo_settings

try:
from django.apps import apps
get_model = apps.get_model
except ImportError:
from django.db.models.loading import get_model

from solo.models import SingletonModel

register = template.Library()


@register.simple_tag(name=solo_settings.GET_SOLO_TEMPLATE_TAG_NAME)
def get_solo(model_path):
def get_solo(model_path: str) -> SingletonModel:
try:
app_label, model_name = model_path.rsplit('.', 1)
except ValueError:
raise template.TemplateSyntaxError(_(
"Templatetag requires the model dotted path: 'app_label.ModelName'. "
"Received '%s'." % model_path
))
model_class = get_model(app_label, model_name)
model_class: type[SingletonModel] = apps.get_model(app_label, model_name)
if not model_class:
raise template.TemplateSyntaxError(_(
"Could not get the model name '%(model)s' from the application "
Expand Down
4 changes: 2 additions & 2 deletions solo/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from django.core.cache import caches
from django.core.files.uploadedfile import SimpleUploadedFile
from django.template import Template, Context
from django.test import TestCase

from django.test.utils import override_settings
from solo.models import get_cache
from solo.tests.models import SiteConfiguration, SiteConfigurationWithExplicitlyGivenId


Expand All @@ -16,7 +16,7 @@ def setUp(self):
'{{ site_config.site_name }}'
'{{ site_config.file.url }}'
)
self.cache = get_cache('default')
self.cache = caches['default']
self.cache_key = SiteConfiguration.get_cache_key()
self.cache.clear()
SiteConfiguration.objects.all().delete()
Expand Down
11 changes: 10 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
python =
3.8: py38-django{32,42}
3.9: py39-django{32,42}
3.10: py310-django{32,42,50}
3.10: py310-django{32,42,50}, type-check
3.11: py311-django{42,50}
3.12: py312-django{42,50}

[tox]
envlist =
type-check
py{38,39,310}-django{32,42}
py{311,312}-django{42,50}

Expand All @@ -33,3 +34,11 @@ deps =
twine
commands =
{envpython} -m twine upload {toxinidir}/dist/*

[testenv:type-check]
skip_install = true
deps =
mypy
django-stubs
Viicos marked this conversation as resolved.
Show resolved Hide resolved
commands =
mypy solo