diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 2aedf7f..98d42c8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.gitignore b/.gitignore index 0bc5cc3..33aa965 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ __pycache__ venv dist files +.mypy_cache .DS_STORE diff --git a/CHANGES b/CHANGES index 4ba29da..f93eb06 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,26 @@ +Unreleased +========== + +django-solo-2.3.0 +================= + +Date: 1 July, 2024 + +* Add typing support +* Deprecate `solo.models.get_cache` +* Switch to `pyproject.toml` +* Switch to Ruff for formatting and linting + +django-solo-2.2.0 +================= + +Date: 1 January, 2024 + +* Add support for Python 3.12 +* Drop support for Python 3.7 +* Add support for Django 5.0 +* Drop support for end of life Django 4.0 and 4.1 + django-solo-2.1.0 ================= diff --git a/MANIFEST.in b/MANIFEST.in index d4da389..348bb6f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include *.md include LICENSE include CHANGES +include solo/py.typed recursive-include solo/templates * recursive-include solo/locale *.mo *.po diff --git a/README.md b/README.md index f567cb4..ca7f30a 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,11 @@ To run the unit tests: 1. Update [`solo/__init__.py`](solo/__init__.py) `version` -2. Make a new release on GitHub +2. Update [`CHANGES`](./CHANGES) + +3. Make a new release on GitHub + +4. Upload release to PyPI ```shell tox -e build diff --git a/examples/config/admin.py b/examples/config/admin.py index fb463f0..b781e93 100644 --- a/examples/config/admin.py +++ b/examples/config/admin.py @@ -1,8 +1,6 @@ from django.contrib import admin -from solo.admin import SingletonModelAdmin - from config.models import SiteConfiguration - +from solo.admin import SingletonModelAdmin admin.site.register(SiteConfiguration, SingletonModelAdmin) diff --git a/examples/config/models.py b/examples/config/models.py index 6079378..d41a30b 100644 --- a/examples/config/models.py +++ b/examples/config/models.py @@ -4,11 +4,11 @@ class SiteConfiguration(SingletonModel): - site_name = models.CharField(max_length=255, default='Site Name') + site_name = models.CharField(max_length=255, default="Site Name") maintenance_mode = models.BooleanField(default=False) def __str__(self): - return u"Site Configuration" + return "Site Configuration" class Meta: verbose_name = "Site Configuration" diff --git a/manage.py b/manage.py index fc138c7..fcb8fc3 100644 --- a/manage.py +++ b/manage.py @@ -3,13 +3,14 @@ Note: For django-solo, this file is used simply to launch the test suite. """ + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -21,5 +22,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3eb255c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[build-system] +requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "django-solo" +description = "Django Solo helps working with singletons" +authors = [{name = "lazybird"}] +maintainers = [ + {name = "John Hagen", email = "johnthagen@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "django>=3.2", + "typing-extensions>=4.0.1; python_version < '3.11'", +] +license = {text = "Creative Commons Attribution 3.0 Unported"} +dynamic = ["version"] + +[tool.setuptools.dynamic] +version = {attr = "solo.__version__"} + +[project.urls] +Homepage = "https://github.com/lazybird/django-solo/" +Source = "https://github.com/lazybird/django-solo/" +Changelog = "https://github.com/lazybird/django-solo/blob/master/CHANGES" + +[tool.mypy] +ignore_missing_imports = true +strict = true +exclude = "solo/tests" + +[tool.ruff] +line-length = 100 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "F", # pyflakes + "E", # pycodestyle + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "RUF", # ruff + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "PTH", # flake8-use-pathlib + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports +] + +ignore = [ + "B904", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index 6b40531..0000000 --- a/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -import re - -from setuptools import setup, find_packages - -README = os.path.join(os.path.dirname(__file__), 'README.md') - -# When running tests using tox, README.md is not found -try: - with open(README) as file: - long_description = file.read() -except Exception: - long_description = '' - - -def get_version(package): - """ - Return package version as listed in `__version__` in `__init__.py`. - """ - with open(os.path.join(package, '__init__.py')) as file: - init_py = file.read() - return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) - - -version = get_version('solo') - -setup( - name='django-solo', - version=version, - description='Django Solo helps working with singletons', - python_requires='>=3.7', - install_requires=['django>=3.2'], - packages=find_packages(), - url='https://github.com/lazybird/django-solo/', - author='lazybird', - long_description=open('README.md').read(), - long_description_content_type='text/markdown', - include_package_data=True, - zip_safe=False, - license='Creative Commons Attribution 3.0 Unported', - classifiers=[ - 'Framework :: Django :: 3.2', - 'Framework :: Django :: 4.0', - 'Framework :: Django :: 4.1', - 'Framework :: Django :: 4.2', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - ] -) diff --git a/solo/__init__.py b/solo/__init__.py index bf13047..0c5b159 100644 --- a/solo/__init__.py +++ b/solo/__init__.py @@ -1,9 +1,11 @@ -"""django-solo helps working with singletons: things like global settings that you want to edit from the admin site. """ -import django +django-solo helps working with singletons: +things like global settings that you want to edit from the admin site. +""" +import django -__version__ = '2.1.0' +__version__ = "2.3.0" if django.VERSION < (3, 2): - default_app_config = 'solo.apps.SoloAppConfig' + default_app_config = "solo.apps.SoloAppConfig" diff --git a/solo/admin.py b/solo/admin.py index a811537..5101972 100644 --- a/solo/admin.py +++ b/solo/admin.py @@ -1,25 +1,30 @@ -from django.urls import re_path +from __future__ import annotations + +from typing import Any + from django.contrib import admin -from django.http import HttpResponseRedirect +from django.db.models import Model +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect +from django.urls import URLPattern, re_path 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 +from solo.models import DEFAULT_SINGLETON_INSTANCE_ID -class SingletonModelAdmin(admin.ModelAdmin): +class SingletonModelAdmin(admin.ModelAdmin): # type: ignore[type-arg] 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 @@ -32,61 +37,68 @@ def get_urls(self): model_name = self.model._meta.module_name.lower() self.model._meta.verbose_name_plural = self.model._meta.verbose_name - url_name_prefix = '%(app_name)s_%(model_name)s' % { - 'app_name': self.model._meta.app_label, - 'model_name': model_name, - } + url_name_prefix = f"{self.model._meta.app_label}_{model_name}" custom_urls = [ - re_path(r'^history/$', - self.admin_site.admin_view(self.history_view), - {'object_id': str(self.singleton_instance_id)}, - name='%s_history' % url_name_prefix), - re_path(r'^$', - self.admin_site.admin_view(self.change_view), - {'object_id': str(self.singleton_instance_id)}, - name='%s_change' % url_name_prefix), + re_path( + r"^history/$", + self.admin_site.admin_view(self.history_view), + {"object_id": str(self.singleton_instance_id)}, + name=f"{url_name_prefix}_history", + ), + re_path( + r"^$", + self.admin_site.admin_view(self.change_view), + {"object_id": str(self.singleton_instance_id)}, + name=f"{url_name_prefix}_change", + ), ] # By inserting the custom URLs first, we overwrite the standard URLs. return custom_urls + urls - def response_change(self, request, obj): - msg = _('%(obj)s was changed successfully.') % { - 'obj': force_str(obj)} - if '_continue' in request.POST: - self.message_user(request, msg + ' ' + - _('You may edit it again below.')) + def response_change(self, request: HttpRequest, obj: Model) -> HttpResponseRedirect: + msg = _("{obj} was changed successfully.").format(obj=force_str(obj)) + if "_continue" in request.POST: + self.message_user(request, msg + " " + _("You may edit it again below.")) return HttpResponseRedirect(request.path) else: 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 + extra_context = {} + 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 + extra_context = {} + 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): - return getattr(self.model, 'singleton_instance_id', DEFAULT_SINGLETON_INSTANCE_ID) + def singleton_instance_id(self) -> int: + return getattr(self.model, "singleton_instance_id", DEFAULT_SINGLETON_INSTANCE_ID) diff --git a/solo/apps.py b/solo/apps.py index b67e59b..7c061db 100644 --- a/solo/apps.py +++ b/solo/apps.py @@ -2,5 +2,5 @@ class SoloAppConfig(AppConfig): - name = 'solo' + name = "solo" verbose_name = "solo" diff --git a/solo/models.py b/solo/models.py index 692d044..f46358c 100644 --- a/solo/models.py +++ b/solo/models.py @@ -1,63 +1,81 @@ +from __future__ import annotations + +import sys +import warnings +from typing import Any + from django.conf import settings +from django.core.cache import BaseCache, 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 +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + + DEFAULT_SINGLETON_INSTANCE_ID = 1 + +def get_cache(cache_name: str) -> BaseCache: + warnings.warn( + "'get_cache' is deprecated and will be removed in django-solo 2.4.0. " + "Instead, use 'caches' from 'django.core.cache'.", + DeprecationWarning, + stacklevel=2, + ) + return caches[cache_name] # type: ignore[no-any-return] # mypy bug, unable to get a MRE + + 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) @classmethod - def clear_cache(cls): - cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE) + 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): - cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE) + 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) + timeout = getattr(settings, "SOLO_CACHE_TIMEOUT", solo_settings.SOLO_CACHE_TIMEOUT) cache.set(cache_key, self, timeout) @classmethod - def get_cache_key(cls): - prefix = getattr(settings, 'SOLO_CACHE_PREFIX', solo_settings.SOLO_CACHE_PREFIX) - return '%s:%s' % (prefix, cls.__name__.lower()) + def get_cache_key(cls) -> str: + prefix = getattr(settings, "SOLO_CACHE_PREFIX", solo_settings.SOLO_CACHE_PREFIX) + return f"{prefix}:{cls.__name__.lower()}" @classmethod - def get_solo(cls): - cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE) + def get_solo(cls) -> 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] + 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] diff --git a/solo/py.typed b/solo/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/solo/settings.py b/solo/settings.py index 8ca9d1a..443e4c0 100644 --- a/solo/settings.py +++ b/solo/settings.py @@ -1,17 +1,17 @@ +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' +SOLO_CACHE_PREFIX = "solo" +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/solo/templatetags/solo_tags.py b/solo/templatetags/solo_tags.py index de0cdfe..a9c9b51 100644 --- a/solo/templatetags/solo_tags.py +++ b/solo/templatetags/solo_tags.py @@ -1,34 +1,29 @@ 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) + 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) + raise template.TemplateSyntaxError( + _( + "Templatetag requires the model dotted path: 'app_label.ModelName'. " + "Received '{model_path}'." + ).format(model_path=model_path) + ) + 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 " - "named '%(app)s'" % { - 'model': model_name, - 'app': app_label, - } - )) + raise template.TemplateSyntaxError( + _("Could not get the model name '{model}' from the application named '{app}'").format( + model=model_name, app=app_label + ) + ) return model_class.get_solo() diff --git a/solo/tests/models.py b/solo/tests/models.py index a424933..9f5ccb9 100644 --- a/solo/tests/models.py +++ b/solo/tests/models.py @@ -5,8 +5,8 @@ class SiteConfiguration(SingletonModel): - site_name = models.CharField(max_length=255, default='Default Config') - file = models.FileField(upload_to='files', default=SimpleUploadedFile("default-file.pdf", None)) + site_name = models.CharField(max_length=255, default="Default Config") + file = models.FileField(upload_to="files", default=SimpleUploadedFile("default-file.pdf", None)) def __str__(self): return "Site Configuration" @@ -17,7 +17,7 @@ class Meta: class SiteConfigurationWithExplicitlyGivenId(SingletonModel): singleton_instance_id = 24 - site_name = models.CharField(max_length=255, default='Default Config') + site_name = models.CharField(max_length=255, default="Default Config") def __str__(self): return "Site Configuration" diff --git a/solo/tests/settings.py b/solo/tests/settings.py index 0fb90ba..33858c9 100644 --- a/solo/tests/settings.py +++ b/solo/tests/settings.py @@ -1,39 +1,39 @@ MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", ) DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'solo-tests.db', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "solo-tests.db", } } INSTALLED_APPS = ( - 'solo', - 'solo.tests', + "solo", + "solo.tests", ) -SECRET_KEY = 'any-key' +SECRET_KEY = "any-key" CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': '127.0.0.1:11211', + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "127.0.0.1:11211", }, } -SOLO_CACHE = 'default' +SOLO_CACHE = "default" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, }, ] -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/solo/tests/tests.py b/solo/tests/tests.py index 6c7ffc8..5262991 100644 --- a/solo/tests/tests.py +++ b/solo/tests/tests.py @@ -1,22 +1,21 @@ +from django.core.cache import caches from django.core.files.uploadedfile import SimpleUploadedFile -from django.template import Template, Context +from django.template import Context, Template 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 class SingletonTest(TestCase): - def setUp(self): self.template = Template( - '{% load solo_tags %}' + "{% load solo_tags %}" '{% get_solo "tests.SiteConfiguration" as site_config %}' - '{{ site_config.site_name }}' - '{{ site_config.file.url }}' + "{{ 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() @@ -27,40 +26,40 @@ def test_template_tag_renders_default_site_config(self): # one to be created automatically with the default name value as # defined in models. output = self.template.render(Context()) - self.assertIn('Default Config', output) + self.assertIn("Default Config", output) def test_template_tag_renders_site_config(self): - SiteConfiguration.objects.create(site_name='Test Config') + SiteConfiguration.objects.create(site_name="Test Config") output = self.template.render(Context()) - self.assertIn('Test Config', output) + self.assertIn("Test Config", output) - @override_settings(SOLO_CACHE='default') + @override_settings(SOLO_CACHE="default") def test_template_tag_uses_cache_if_enabled(self): - SiteConfiguration.objects.create(site_name='Config In Database') - fake_configuration = {'site_name': 'Config In Cache'} + SiteConfiguration.objects.create(site_name="Config In Database") + fake_configuration = {"site_name": "Config In Cache"} self.cache.set(self.cache_key, fake_configuration, 10) output = self.template.render(Context()) - self.assertNotIn('Config In Database', output) - self.assertNotIn('Default Config', output) - self.assertIn('Config In Cache', output) + self.assertNotIn("Config In Database", output) + self.assertNotIn("Default Config", output) + self.assertIn("Config In Cache", output) @override_settings(SOLO_CACHE=None) def test_template_tag_uses_database_if_cache_disabled(self): - SiteConfiguration.objects.create(site_name='Config In Database') - fake_configuration = {'site_name': 'Config In Cache'} + SiteConfiguration.objects.create(site_name="Config In Database") + fake_configuration = {"site_name": "Config In Cache"} self.cache.set(self.cache_key, fake_configuration, 10) output = self.template.render(Context()) - self.assertNotIn('Config In Cache', output) - self.assertNotIn('Default Config', output) - self.assertIn('Config In Database', output) + self.assertNotIn("Config In Cache", output) + self.assertNotIn("Default Config", output) + self.assertIn("Config In Database", output) - @override_settings(SOLO_CACHE='default') + @override_settings(SOLO_CACHE="default") def test_delete_if_cache_enabled(self): self.assertEqual(SiteConfiguration.objects.count(), 0) self.assertIsNone(self.cache.get(self.cache_key)) one_cfg = SiteConfiguration.get_solo() - one_cfg.site_name = 'TEST SITE PLEASE IGNORE' + one_cfg.site_name = "TEST SITE PLEASE IGNORE" one_cfg.save() self.assertEqual(SiteConfiguration.objects.count(), 1) self.assertIsNotNone(self.cache.get(self.cache_key)) @@ -68,35 +67,36 @@ def test_delete_if_cache_enabled(self): one_cfg.delete() self.assertEqual(SiteConfiguration.objects.count(), 0) self.assertIsNone(self.cache.get(self.cache_key)) - self.assertEqual(SiteConfiguration.get_solo().site_name, 'Default Config') + self.assertEqual(SiteConfiguration.get_solo().site_name, "Default Config") @override_settings(SOLO_CACHE=None) def test_delete_if_cache_disabled(self): # As above, but without the cache checks self.assertEqual(SiteConfiguration.objects.count(), 0) one_cfg = SiteConfiguration.get_solo() - one_cfg.site_name = 'TEST (uncached) SITE PLEASE IGNORE' + one_cfg.site_name = "TEST (uncached) SITE PLEASE IGNORE" one_cfg.save() self.assertEqual(SiteConfiguration.objects.count(), 1) one_cfg.delete() self.assertEqual(SiteConfiguration.objects.count(), 0) - self.assertEqual(SiteConfiguration.get_solo().site_name, 'Default Config') + self.assertEqual(SiteConfiguration.get_solo().site_name, "Default Config") - @override_settings(SOLO_CACHE='default') + @override_settings(SOLO_CACHE="default") def test_file_upload_if_cache_enabled(self): - cfg = SiteConfiguration.objects.create(site_name='Test Config', file=SimpleUploadedFile("file.pdf", None)) + cfg = SiteConfiguration.objects.create( + site_name="Test Config", file=SimpleUploadedFile("file.pdf", None) + ) output = self.template.render(Context()) self.assertIn(cfg.file.url, output) - @override_settings(SOLO_CACHE_PREFIX='other') + @override_settings(SOLO_CACHE_PREFIX="other") def test_cache_prefix_overriding(self): key = SiteConfiguration.get_cache_key() - prefix = key.partition(':')[0] - self.assertEqual(prefix, 'other') + prefix = key.partition(":")[0] + self.assertEqual(prefix, "other") class SingletonWithExplicitIdTest(TestCase): - def setUp(self): SiteConfigurationWithExplicitlyGivenId.objects.all().delete() diff --git a/tox.ini b/tox.ini index a881a6e..5f6fd17 100644 --- a/tox.ini +++ b/tox.ini @@ -1,24 +1,24 @@ # Configure which test environments are run for each Github Actions Python version. [gh-actions] python = - 3.7: py37-django{32} - 3.8: py38-django{32,40,41,42} - 3.9: py39-django{32,40,41,42} - 3.10: py310-django{32,40,41,42} - 3.11: py311-django{40,41,42} + 3.8: py38-django{32,42} + 3.9: py39-django{32,42} + 3.10: py310-django{32,42,50}, type-check, lint + 3.11: py311-django{42,50} + 3.12: py312-django{42,50} [tox] envlist = - py{37}-django{32} - py{38,39,310}-django{32,40,41,42} - py{311}-django{40,41,42} + type-check + lint + py{38,39,310}-django{32,42} + py{311,312}-django{42,50} [testenv] deps = django32: Django>=3.2,<4.0 - django40: Django>=4.0,<4.1 - django41: Django>=4.1,<4.2 django42: Django>=4.2,<4.3 + django50: Django>=5.0,<5.1 commands = {envpython} {toxinidir}/manage.py test solo --settings=solo.tests.settings @@ -35,3 +35,19 @@ deps = twine commands = {envpython} -m twine upload {toxinidir}/dist/* + +[testenv:type-check] +skip_install = true +deps = + mypy==1.8.0 + django-stubs==4.2.7 +commands = + mypy solo + +[testenv:lint] +skip_install = true +deps = + ruff==0.5.0 +commands = + ruff format --check + ruff check