From 3c9c7bd718a577d38c58d57a889daf0d719c3968 Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:39:43 +0200 Subject: [PATCH] Add Ruff linting (#136) --- .github/workflows/python.yml | 4 +-- CHANGES | 2 ++ examples/config/admin.py | 4 +-- examples/config/models.py | 4 +-- manage.py | 5 +-- pyproject.toml | 23 ++++++++++++ solo/__init__.py | 10 +++--- solo/admin.py | 61 ++++++++++++++++++-------------- solo/apps.py | 2 +- solo/models.py | 12 +++---- solo/settings.py | 12 +++---- solo/templatetags/solo_tags.py | 24 ++++++------- solo/tests/models.py | 6 ++-- solo/tests/settings.py | 38 ++++++++++---------- solo/tests/tests.py | 64 +++++++++++++++++----------------- tox.ini | 11 +++++- 16 files changed, 160 insertions(+), 122 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 11ea155..98d42c8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -9,9 +9,9 @@ jobs: matrix: 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/CHANGES b/CHANGES index 55fefa1..b2e46b7 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,8 @@ Unreleased * Add typing support * Deprecate `solo.models.get_cache` +* Switch to `pyproject.toml` +* Switch to Ruff for formatting and linting django-solo-2.2.0 ================= 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 index a9bdbb9..3eb255c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,3 +44,26 @@ Changelog = "https://github.com/lazybird/django-solo/blob/master/CHANGES" 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/solo/__init__.py b/solo/__init__.py index 8567c1e..62fc682 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.2.0' +__version__ = "2.2.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 4bc8922..5101972 100644 --- a/solo/admin.py +++ b/solo/admin.py @@ -2,15 +2,15 @@ from typing import Any -from django.db.models import Model -from django.urls import URLPattern, re_path from django.contrib import admin +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): # type: ignore[type-arg] @@ -37,42 +37,47 @@ def get_urls(self) -> list[URLPattern]: 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: HttpRequest, obj: Model) -> HttpResponseRedirect: - 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.')) + 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: HttpRequest, object_id: str, form_url: str = '', extra_context: dict[str, Any] | None = None) -> HttpResponse: + 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().change_view( request, @@ -81,10 +86,12 @@ def change_view(self, request: HttpRequest, object_id: str, form_url: str = '', extra_context=extra_context, ) - def history_view(self, request: HttpRequest, object_id: str, extra_context: dict[str, Any] | None = None) -> HttpResponse: + 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().history_view( request, @@ -94,4 +101,4 @@ def history_view(self, request: HttpRequest, object_id: str, extra_context: dict @property def singleton_instance_id(self) -> int: - return getattr(self.model, 'singleton_instance_id', DEFAULT_SINGLETON_INSTANCE_ID) + 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 0ee4052..f46358c 100644 --- a/solo/models.py +++ b/solo/models.py @@ -46,29 +46,29 @@ def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]: @classmethod def clear_cache(cls) -> None: - cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE) + cache_name = getattr(settings, "SOLO_CACHE", solo_settings.SOLO_CACHE) if cache_name: cache = caches[cache_name] cache_key = cls.get_cache_key() cache.delete(cache_key) def set_to_cache(self) -> None: - cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE) + cache_name = getattr(settings, "SOLO_CACHE", solo_settings.SOLO_CACHE) if not cache_name: return None 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) -> str: - prefix = getattr(settings, 'SOLO_CACHE_PREFIX', solo_settings.SOLO_CACHE_PREFIX) - return '%s:%s' % (prefix, cls.__name__.lower()) + prefix = getattr(settings, "SOLO_CACHE_PREFIX", solo_settings.SOLO_CACHE_PREFIX) + return f"{prefix}:{cls.__name__.lower()}" @classmethod def get_solo(cls) -> Self: - cache_name = getattr(settings, 'SOLO_CACHE', solo_settings.SOLO_CACHE) + cache_name = getattr(settings, "SOLO_CACHE", solo_settings.SOLO_CACHE) if not cache_name: obj, _ = cls.objects.get_or_create(pk=cls.singleton_instance_id) return obj # type: ignore[return-value] diff --git a/solo/settings.py b/solo/settings.py index ecff936..443e4c0 100644 --- a/solo/settings.py +++ b/solo/settings.py @@ -3,13 +3,9 @@ from django.conf import settings # template parameters -GET_SOLO_TEMPLATE_TAG_NAME: str = 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: bool = 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. @@ -17,5 +13,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 fda21ff..a9c9b51 100644 --- a/solo/templatetags/solo_tags.py +++ b/solo/templatetags/solo_tags.py @@ -11,19 +11,19 @@ @register.simple_tag(name=solo_settings.GET_SOLO_TEMPLATE_TAG_NAME) 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 - )) + 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 ece04c8..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.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 = caches['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 56bd9e9..5f6fd17 100644 --- a/tox.ini +++ b/tox.ini @@ -3,13 +3,14 @@ python = 3.8: py38-django{32,42} 3.9: py39-django{32,42} - 3.10: py310-django{32,42,50}, type-check + 3.10: py310-django{32,42,50}, type-check, lint 3.11: py311-django{42,50} 3.12: py312-django{42,50} [tox] envlist = type-check + lint py{38,39,310}-django{32,42} py{311,312}-django{42,50} @@ -42,3 +43,11 @@ deps = django-stubs==4.2.7 commands = mypy solo + +[testenv:lint] +skip_install = true +deps = + ruff==0.5.0 +commands = + ruff format --check + ruff check