diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index fecc132..131393b 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -17,9 +17,7 @@ jobs: strategy: matrix: toxenv: - - isort - - black - # - flake8 + - ruff - docs steps: - uses: actions/checkout@v4 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1ee73cc..0c1045f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -23,8 +23,8 @@ checks: * Pull requests must be accompanied by tests. We use ``pytest`` and prefer using this testing style over Django's ``django.test.TestCase``. * Ideally, documentation updates are included in a pull request. -* Imports are sorted using ``isort``, while code is formatted using ``black``. There - are tox environments and CI checks in place to check/enforce this. +* Code formatting and linting is done with ``ruff``. There are tox environments and CI + checks in place to check/enforce this. * Follow Django's code style where possible. * Keep commits atomic - one commit should only concern one topic. Bugfixes typically have one commit for the regression test and one commit with the fix. @@ -100,8 +100,8 @@ or to build the full test matrix .. code-block:: bash - black . - isort . + ruff format . + ruff check --fix . Should be sufficient. Consider using a pre-commit hook to automate this. diff --git a/README.md b/README.md index 372a269..7fd0def 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Manage cookie information and let visitors give or reject consent for them. ![License](https://img.shields.io/pypi/l/django-cookie-consent) [![Build status][badge:GithubActions:CI]][GithubActions:CI] [![Code Quality][badge:GithubActions:CQ]][GithubActions:CQ] -[![Code style: black][badge:black]][black] +[![Code style: ruff][badge:ruff]][ruff] [![Test coverage][badge:codecov]][codecov] [![Documentation][badge:docs]][docs] @@ -36,8 +36,8 @@ from the `docs` directory in this repository. [badge:GithubActions:CI]: https://github.com/django-commons/django-cookie-consent/workflows/Run%20CI/badge.svg [GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Code+quality+checks%22 [badge:GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/workflows/Code%20quality%20checks/badge.svg -[black]: https://github.com/psf/black -[badge:black]: https://img.shields.io/badge/code%20style-black-000000.svg +[ruff]: https://github.com/astral-sh/ruff +[badge:ruff]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json [codecov]: https://codecov.io/gh/django-commons/django-cookie-consent [badge:codecov]: https://codecov.io/gh/django-commons/django-cookie-consent/branch/master/graph/badge.svg [docs]: https://django-cookie-consent.readthedocs.io/en/latest/?badge=latest diff --git a/cookie_consent/admin.py b/cookie_consent/admin.py index 9aef7bb..c7e6e0f 100644 --- a/cookie_consent/admin.py +++ b/cookie_consent/admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib import admin from .conf import settings diff --git a/cookie_consent/cache.py b/cookie_consent/cache.py index 9c61fb8..d5c645b 100644 --- a/cookie_consent/cache.py +++ b/cookie_consent/cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.core.cache import caches from .conf import settings diff --git a/cookie_consent/conf.py b/cookie_consent/conf.py index 08bcd3a..4e23d48 100644 --- a/cookie_consent/conf.py +++ b/cookie_consent/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.conf import settings # NOQA from appconf import AppConf diff --git a/cookie_consent/middleware.py b/cookie_consent/middleware.py index 945d142..e5f0353 100644 --- a/cookie_consent/middleware.py +++ b/cookie_consent/middleware.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- -from typing import Optional - from .cache import all_cookie_groups from .conf import settings from .util import get_cookie_dict_from_request, is_cookie_consent_enabled -def _should_delete_cookie(group_version: Optional[str]) -> bool: +def _should_delete_cookie(group_version: str | None) -> bool: # declined after it was accepted (and set) before if group_version == settings.COOKIE_CONSENT_DECLINE: return True diff --git a/cookie_consent/migrations/0001_initial.py b/cookie_consent/migrations/0001_initial.py index 06f893f..e165f4a 100644 --- a/cookie_consent/migrations/0001_initial.py +++ b/cookie_consent/migrations/0001_initial.py @@ -68,7 +68,8 @@ class Migration(migrations.Migration): validators=[ django.core.validators.RegexValidator( re.compile("^[-_a-zA-Z0-9]+$"), - "Enter a valid 'varname' consisting of letters, numbers, underscores or hyphens.", + "Enter a valid 'varname' consisting of letters, " + "numbers, underscores or hyphens.", "invalid", ) ], diff --git a/cookie_consent/migrations/0003_alter_cookiegroup_varname.py b/cookie_consent/migrations/0003_alter_cookiegroup_varname.py index 96a62d8..532761c 100644 --- a/cookie_consent/migrations/0003_alter_cookiegroup_varname.py +++ b/cookie_consent/migrations/0003_alter_cookiegroup_varname.py @@ -7,7 +7,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cookie_consent", "0002_auto__add_logitem"), ] @@ -22,7 +21,8 @@ class Migration(migrations.Migration): validators=[ django.core.validators.RegexValidator( re.compile("^[-_a-zA-Z0-9]+$"), - "Enter a valid 'varname' consisting of letters, numbers, underscores or hyphens.", + "Enter a valid 'varname' consisting of letters, numbers, " + "underscores or hyphens.", "invalid", ) ], diff --git a/cookie_consent/migrations/0004_cookie_natural_key.py b/cookie_consent/migrations/0004_cookie_natural_key.py index 6dfa54a..7bb5a7e 100644 --- a/cookie_consent/migrations/0004_cookie_natural_key.py +++ b/cookie_consent/migrations/0004_cookie_natural_key.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cookie_consent", "0003_alter_cookiegroup_varname"), ] diff --git a/cookie_consent/models.py b/cookie_consent/models.py index c9a66ba..cf96e25 100644 --- a/cookie_consent/models.py +++ b/cookie_consent/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import re from typing import TypedDict @@ -144,7 +143,7 @@ class Meta: ordering = ["-created"] def __str__(self): - return "%s %s%s" % (self.name, self.domain, self.path) + return f"{self.name} {self.domain}{self.path}" @clear_cache_after def save(self, *args, **kwargs): @@ -161,7 +160,8 @@ def natural_key(self): @property def varname(self): - return "%s=%s:%s" % (self.cookiegroup.varname, self.name, self.domain) + group_varname = self.cookiegroup.varname + return f"{group_varname}={self.name}:{self.domain}" def get_version(self): return self.created.isoformat() @@ -185,10 +185,10 @@ class LogItem(models.Model): version = models.CharField(_("Version"), max_length=32) created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True) - def __str__(self): - return "%s %s" % (self.cookiegroup.name, self.version) - class Meta: verbose_name = _("Log item") verbose_name_plural = _("Log items") ordering = ["-created"] + + def __str__(self): + return f"{self.cookiegroup.name} {self.version}" diff --git a/cookie_consent/templatetags/__init__.py b/cookie_consent/templatetags/__init__.py index faa18be..4265cc3 100644 --- a/cookie_consent/templatetags/__init__.py +++ b/cookie_consent/templatetags/__init__.py @@ -1,2 +1 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- diff --git a/cookie_consent/templatetags/cookie_consent_tags.py b/cookie_consent/templatetags/cookie_consent_tags.py index 6ecb5a4..8ae23d9 100644 --- a/cookie_consent/templatetags/cookie_consent_tags.py +++ b/cookie_consent/templatetags/cookie_consent_tags.py @@ -97,6 +97,7 @@ def get_accept_cookie_groups_cookie_string(request, cookie_groups): # pragma: n "Cookie string template tags for JS are deprecated and will be removed " "in django-cookie-consent 1.0", DeprecationWarning, + stacklevel=1, ) cookie_dic = get_cookie_dict_from_request(request) for cookie_group in cookie_groups: @@ -113,6 +114,7 @@ def get_decline_cookie_groups_cookie_string(request, cookie_groups): "Cookie string template tags for JS are deprecated and will be removed " "in django-cookie-consent 1.0", DeprecationWarning, + stacklevel=1, ) cookie_dic = get_cookie_dict_from_request(request) for cookie_group in cookie_groups: @@ -133,12 +135,14 @@ def js_type_for_cookie_consent(request, varname, cookie=None): alert("Social cookie accepted"); """ - # This approach doesn't work with page caches and/or strict Content-Security-Policies - # (unless you use nonces, which again doesn't work with aggressive page caching). + # This approach doesn't work with page caches and/or strict + # Content-Security-Policies (unless you use nonces, which again doesn't work with + # aggressive page caching). warnings.warn( "Template tags for use in/with JS are deprecated and will be removed " "in django-cookie-consent 1.0", DeprecationWarning, + stacklevel=1, ) enabled = is_cookie_consent_enabled(request) if not enabled: @@ -162,6 +166,12 @@ def accepted_cookies(request): {{ request|accepted_cookies }} """ + warnings.warn( + "The 'accepted_cookies' template filter is deprecated and will be removed" + "in django-cookie-consent 1.0.", + DeprecationWarning, + stacklevel=1, + ) return [c.varname for c in get_accepted_cookies(request)] diff --git a/cookie_consent/urls.py b/cookie_consent/urls.py index fae8d04..6c89b2f 100644 --- a/cookie_consent/urls.py +++ b/cookie_consent/urls.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.urls import path, re_path from django.views.decorators.csrf import csrf_exempt diff --git a/cookie_consent/util.py b/cookie_consent/util.py index b50970f..7f5b8e7 100644 --- a/cookie_consent/util.py +++ b/cookie_consent/util.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- import datetime import logging -from typing import Dict, Union from .cache import all_cookie_groups, get_cookie, get_cookie_group from .conf import settings @@ -13,7 +11,7 @@ KEY_VALUE_SEP = "=" -def parse_cookie_str(cookie: str) -> Dict[str, str]: +def parse_cookie_str(cookie: str) -> dict[str, str]: if not cookie: return {} @@ -171,7 +169,7 @@ def are_all_cookies_accepted(request): ) -def _get_cookie_groups_by_state(request, state: Union[bool, None]): +def _get_cookie_groups_by_state(request, state: bool | None): return [ cookie_group for cookie_group in get_cookie_groups() @@ -219,7 +217,7 @@ def get_cookie_string(cookie_dic): expires = datetime.datetime.now() + datetime.timedelta( seconds=settings.COOKIE_CONSENT_MAX_AGE ) - cookie_str = "%s=%s; expires=%s; path=/" % ( + cookie_str = "{}={}; expires={}; path=/".format( settings.COOKIE_CONSENT_NAME, dict_to_cookie_str(cookie_dic), expires.strftime("%a, %d %b %Y %H:%M:%S GMT"), @@ -239,5 +237,5 @@ def get_accepted_cookies(request): continue for cookie in cookie_group.cookie_set.all(): if version >= cookie.get_version(): - accepted_cookies.append(cookie) + accepted_cookies.append(cookie) # noqa: PERF401 return accepted_cookies diff --git a/cookie_consent/views.py b/cookie_consent/views.py index b756f23..aae3cf6 100644 --- a/cookie_consent/views.py +++ b/cookie_consent/views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib.auth.views import RedirectURLMixin from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse diff --git a/docs/conf.py b/docs/conf.py index e01207b..eb4dc9d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import sys from pathlib import Path @@ -14,7 +13,7 @@ django.setup() -from cookie_consent import __version__ # isort:skip +from cookie_consent import __version__ # noqa: E402 # -- General configuration ----------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index d59d3c4..beedd79 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ Django cookie consent Manage cookie information and let visitors give or reject consent for them. -|build-status| |code-quality| |black| |coverage| |docs| +|build-status| |code-quality| |ruff| |coverage| |docs| |python-versions| |django-versions| |pypi-version| @@ -63,8 +63,8 @@ Indices and tables :alt: Code quality checks :target: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Code+quality+checks%22 -.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black +.. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff .. |coverage| image:: https://codecov.io/gh/django-commons/django-cookie-consent/branch/master/graph/badge.svg :target: https://codecov.io/gh/django-commons/django-cookie-consent diff --git a/pyproject.toml b/pyproject.toml index efbd593..28a73bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,16 +45,12 @@ Changelog = "https://github.com/django-commons/django-cookie-consent/blob/master [project.optional-dependencies] tests = [ "pytest", + "pytest-cov", "pytest-django", "pytest-playwright", "hypothesis", "tox", - "isort", - "black", - "flake8", -] -coverage = [ - "pytest-cov", + "ruff", ] docs = [ "sphinx", @@ -71,14 +67,6 @@ version = {attr = "cookie_consent.__version__"} include = ["cookie_consent*"] namespaces = true -[tool.isort] -profile = "black" -combine_as_imports = true -skip = ["env", ".tox", ".history", ".eggs"] -known_django = "django" -known_first_party="cookie_consent" -sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] - [tool.pytest.ini_options] testpaths = ["tests"] DJANGO_SETTINGS_MODULE = "testapp.settings" @@ -105,3 +93,30 @@ exclude_also = [ "\\.\\.\\.", "\\bpass$", ] + +[tool.ruff.lint] +extend-select = [ + "UP", # pyupgrade + "DJ", # django + "LOG", # logging + "G", + "I", # isort + "E", # pycodestyle + "F", # pyflakes + "PERF",# perflint + "B", # flake8-bugbear +] + +[tool.ruff.lint.isort] +combine-as-imports = true +section-order = [ + "future", + "standard-library", + "django", + "third-party", + "first-party", + "local-folder", +] + +[tool.ruff.lint.isort.sections] +"django" = ["django"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index dd2a903..0000000 --- a/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[pep8] -[flake8] -max-line-length=88 -exclude=env,.tox,docs diff --git a/testapp/views.py b/testapp/views.py index afd8b26..dc48390 100644 --- a/testapp/views.py +++ b/testapp/views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.views.generic import TemplateView from cookie_consent.util import get_cookie_value_from_request diff --git a/tests/test_cache.py b/tests/test_cache.py index c3d12b1..5e5e9ea 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.test import TestCase, override_settings from cookie_consent.cache import delete_cache, get_cookie, get_cookie_group diff --git a/tests/test_models.py b/tests/test_models.py index 5e51c1d..02fa462 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +import string from copy import deepcopy from django.conf import settings @@ -6,6 +6,9 @@ from django.core.exceptions import ValidationError from django.test import TestCase, override_settings +import pytest +from hypothesis import given, strategies as st + from cookie_consent.cache import CACHE_KEY, delete_cache from cookie_consent.models import Cookie, CookieGroup, validate_cookie_name @@ -57,7 +60,8 @@ def test_bulk_delete(self): id=self.cookie_group.id ).delete() - # Deleting a CookieGroup also deletes the associated Cookies, that's why we expect a count of 2. + # Deleting a CookieGroup also deletes the associated Cookies, that's why we + # expect a count of 2. self.assertEqual(deleted_objs_count, 2) self.assertCacheNotPopulated() @@ -109,16 +113,27 @@ def test_bulk_update(self): self.assertCacheNotPopulated() -class ValidateCookieNameTest(TestCase): - def test_valid(self): - validate_cookie_name("_foo-bar") - - def test_invalid(self): - invalid_names = ( - "space inside", - "a!b", - "$", - ) - for name in invalid_names: - with self.assertRaises(ValidationError): - validate_cookie_name("no spaces") +@given( + name=st.text( + alphabet=string.ascii_letters + string.digits + "-_", + min_size=1, + ) +) +def test_valid_cookie_name_does_not_raise(name): + try: + validate_cookie_name(name) + except ValidationError: + pytest.fail(reason=f"Expected {name} to be valid") + + +@pytest.mark.parametrize( + "name", + ( + "space inside", + "a!b", + "$", + ), +) +def test_invalid_cookie_name_raises(name: str): + with pytest.raises(ValidationError): + validate_cookie_name(name) diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index f01ba3c..cededad 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -1,12 +1,12 @@ from textwrap import dedent -from typing import Any, Dict, Optional +from typing import Any from django.template import Context, Template import pytest -def render(tpl: str, context: Optional[Dict[str, Any]] = None) -> str: +def render(tpl: str, context: dict[str, Any] | None = None) -> str: template = Template(dedent(tpl).strip()) return template.render(Context(context)) diff --git a/tests/test_util.py b/tests/test_util.py index d9c4659..d763873 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- from datetime import datetime -from django.http import parse_cookie from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings diff --git a/tests/test_views.py b/tests/test_views.py index fe48a1e..dd7c13e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.test import TestCase from django.test.utils import override_settings from django.urls import reverse diff --git a/tox.ini b/tox.ini index 49f10c3..b619957 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,7 @@ envlist = py{310,311,312}-django{42,51,52} py{313}-django{51,52} - isort - black - ; flake8 + ruff docs skip_missing_interpreters = true @@ -52,20 +50,12 @@ commands = --cov --cov-report xml:reports/coverage-{envname}.xml \ {posargs} -[testenv:isort] +[testenv:ruff] extras = tests skipsdist = True -commands = isort --check-only --diff . - -[testenv:black] -extras = tests -skipsdist = True -commands = black --check cookie_consent docs tests testapp - -[testenv:flake8] -extras = tests -skipsdist = True -commands = flake8 . +commands = + ruff check --output-format=github . + ruff format --check [testenv:docs] basepython=python