diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 5840e7ac..00000000 --- a/.flake8 +++ /dev/null @@ -1,8 +0,0 @@ -[flake8] -# Use a line length of 120 instead of pep8's default 79 -max-line-length = 120 -# Files not checked: -# - migrations: most of these are autogenerated and don't need a check -# - docs: contains autogenerated code that doesn't need a check -exclude = */migrations/*,docs -ignore = E731 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 726214a2..91a2d18b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,8 +15,9 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [ 3.8, 3.9, '3.10', '3.11', 3.12 ] + python-version: [ 3.8, 3.9, '3.10', 3.11, 3.12 ] steps: - uses: actions/checkout@v4 @@ -57,11 +58,7 @@ jobs: python -m pip install tox-gh-actions if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + run: ruff check --output-format=github . typecheck: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1646cb0..0fb46da5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,11 @@ repos: -- repo: https://github.com/pycqa/flake8 - rev: '7.1.1' - hooks: - - id: flake8 - exclude: docs/conf.py|test_settings.py - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v5.0.0' hooks: - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.3 + hooks: + - id: ruff diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5469d078..abf6ccce 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -68,7 +68,7 @@ To be mergeable, patches must: - not change existing tests without a *very* good reason, - add tests for new code (bug fixes should include regression tests, new features should have relevant tests), -- not introduce any new flake8_ errors (run ``./run.sh lint``), +- not introduce any new ruff_ errors (run ``./run.sh lint``), - not introduce any new mypy_ errors (run ``./run.sh typecheck``), - include updated source translations (run ``./run.sh makemessages`` and ``./run.sh compilemessages``), - document any new features, and @@ -80,6 +80,6 @@ with it. .. _open a new issue: https://github.com/jazzband/django-waffle/issues/new .. _Fork: https://github.com/jazzband/django-waffle/fork -.. _flake8: https://pypi.python.org/pypi/flake8 +.. _ruff: https://pypi.python.org/pypi/ruff .. _mypy: https://www.mypy-lang.org/ .. _good commit message: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html diff --git a/docs/about/contributing.rst b/docs/about/contributing.rst index 8c27b249..0499b6d4 100644 --- a/docs/about/contributing.rst +++ b/docs/about/contributing.rst @@ -60,7 +60,7 @@ To be mergeable, patches must: - not change existing tests without a *very* good reason, - add tests for new code (bug fixes should include regression tests, new features should have relevant tests), -- not introduce any new flake8_ errors (run ``./run.sh lint``), +- not introduce any new ruff_ errors (run ``./run.sh lint``), - document any new features, and - have a `good commit message`_. @@ -70,5 +70,5 @@ with it. .. _open a new issue: https://github.com/jazzband/django-waffle/issues/new .. _Fork: https://github.com/jazzband/django-waffle/fork -.. _flake8: https://pypi.python.org/pypi/flake8 +.. _ruff: https://pypi.python.org/pypi/ruff .. _good commit message: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html diff --git a/docs/conf.py b/docs/conf.py index 993dfeb7..f65dc044 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/docs/starting/configuring.rst b/docs/starting/configuring.rst index e09341fb..fe48bcb2 100644 --- a/docs/starting/configuring.rst +++ b/docs/starting/configuring.rst @@ -100,7 +100,7 @@ behavior. The value describes the level of wanted warning, possible values are all levels know by pythons default logging, e.g. ``logging.WARNING``. Defaults to ``None``. - + ``WAFFLE_ENABLE_ADMIN_PAGES`` Enables the default admin pages for Waffle models. This is True by default, diff --git a/docs/usage/decorators.rst b/docs/usage/decorators.rst index 47e0eab4..b787826e 100644 --- a/docs/usage/decorators.rst +++ b/docs/usage/decorators.rst @@ -25,7 +25,7 @@ Flags @waffle_flag('flag_name') def myview(request): pass - + @waffle_flag('flag_name', 'url_name_to_redirect_to') def myotherview(request): pass diff --git a/docs/usage/templates.rst b/docs/usage/templates.rst index 903b3724..bc32f060 100644 --- a/docs/usage/templates.rst +++ b/docs/usage/templates.rst @@ -11,7 +11,7 @@ features on the front-end. It includes support for both Django's built-in templates and for Jinja2_. .. warning:: - + Before using samples in templates, see the warning in the :ref:`Sample chapter `. @@ -92,7 +92,7 @@ Switches -------- :: - + {% if waffle.switch('switch_name') %} switch_name is active! {% endif %} diff --git a/docs/usage/views.rst b/docs/usage/views.rst index e82d8ab8..966cebf1 100644 --- a/docs/usage/views.rst +++ b/docs/usage/views.rst @@ -49,5 +49,5 @@ Samples Returns ``True`` if the sample is active, else ``False``. .. warning:: - + See the warning in the :ref:`Sample chapter `. diff --git a/pyproject.toml b/pyproject.toml index dad47045..17b00a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,3 +62,90 @@ strict_equality = true [[tool.mypy.overrides]] module = ["django.*"] ignore_missing_imports = true + +[tool.ruff] +line-length = 120 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "AIR", # Airflow + "ASYNC", # flake8-async + "BLE", # flake8-blind-except + "C90", # McCabe cyclomatic complexity + "DJ", # flake8-django + "DTZ", # flake8-datetimez + "E", # pycodestyle errors + "F", # Pyflakes + "FIX", # flake8-fixme + "FLY", # flynt + "G", # flake8-logging-format + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "NPY", # NumPy-specific rules + "PD", # pandas-vet + "PIE", # flake8-pie + "PL", # Pylint + "PYI", # flake8-pyi + "RSE", # flake8-raise + "SLOT", # flake8-slots + "T10", # flake8-debugger + "T20", # flake8-print + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 + # "A", # flake8-builtins + # "ANN", # flake8-annotations + # "ARG", # flake8-unused-arguments + # "B", # flake8-bugbear + # "C4", # flake8-comprehensions + # "COM", # flake8-commas + # "CPY", # flake8-copyright + # "D", # pydocstyle + # "EM", # flake8-errmsg + # "ERA", # eradicate + # "EXE", # flake8-executable + # "FA", # flake8-future-annotations + # "FBT", # flake8-boolean-trap + # "I", # isort + # "ISC", # flake8-implicit-str-concat + # "N", # pep8-naming + # "PERF", # Perflint + # "PGH", # pygrep-hooks + # "PT", # flake8-pytest-style + # "PTH", # flake8-use-pathlib + # "Q", # flake8-quotes + # "RET", # flake8-return + # "RUF", # Ruff-specific rules + # "S", # flake8-bandit + # "SIM", # flake8-simplify + # "SLF", # flake8-self + # "TCH", # flake8-type-checking + # "TRY", # tryceratops +] +# Files not checked: +# - migrations: most of these are autogenerated and don't need a check +# - docs: contains autogenerated code that doesn't need a check +exclude = [ + "*/migrations/*", + "docs", +] +ignore = ["F401"] + +[tool.ruff.lint.mccabe] +max-complexity = 23 + +[tool.ruff.lint.per-file-ignores] +"docs/conf.py" = ["INP001"] +"test_app/models.py" = ["DJ008"] # FIXME +"waffle/models.py" = ["DJ012", "PYI019"] # FIXME + +[tool.ruff.lint.pylint] +allow-magic-value-types = ["float", "int", "str"] +max-args = 6 # default is 5 +max-branches = 23 # default is 12 +max-returns = 13 # default is 6 +max-statements = 51 # default is 50 diff --git a/requirements.txt b/requirements.txt index fc631674..a7a45320 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,6 @@ Django django-jinja>=2.4.1,<3 transifex-client -flake8 mypy +ruff tox diff --git a/requirements/test.txt b/requirements/test.txt index cc984225..0b86f4f4 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,2 +1,2 @@ -flake8 +ruff tox diff --git a/run.sh b/run.sh index 36c86ec1..c2ac5e8d 100755 --- a/run.sh +++ b/run.sh @@ -6,7 +6,7 @@ export DJANGO_SETTINGS_MODULE="test_settings" usage() { echo "USAGE: $0 [command]" echo " test - run the waffle tests" - echo " lint - run flake8" + echo " lint - run ruff" echo " typecheck - run mypy" echo " shell - open the Django shell" echo " makemigrations - create a schema migration" @@ -20,7 +20,7 @@ case "$CMD" in "test" ) DJANGO_SETTINGS_MODULE=test_settings django-admin test waffle $@ ;; "lint" ) - flake8 waffle $@ ;; + ruff check ;; "typecheck" ) mypy waffle $@ ;; "shell" ) diff --git a/test_settings.py b/test_settings.py index 97f15ce9..d19ee4de 100644 --- a/test_settings.py +++ b/test_settings.py @@ -10,7 +10,7 @@ # Make filepaths relative to settings. ROOT = os.path.dirname(os.path.abspath(__file__)) -path = lambda *a: os.path.join(ROOT, *a) +path = lambda *a: os.path.join(ROOT, *a) # noqa: E731 DEBUG = True TEST_RUNNER = 'django.test.runner.DiscoverRunner' diff --git a/waffle/__init__.py b/waffle/__init__.py index 36cbd438..5e979ca3 100755 --- a/waffle/__init__.py +++ b/waffle/__init__.py @@ -42,7 +42,7 @@ def get_waffle_sample_model() -> type[AbstractBaseSample]: def get_waffle_model(setting_name: str) -> ( - type[AbstractBaseFlag] | type[AbstractBaseSwitch] | type[AbstractBaseSample] + type[AbstractBaseFlag | AbstractBaseSwitch | AbstractBaseSample] ): """ Returns the waffle Flag model that is active in this project. @@ -63,12 +63,8 @@ def get_waffle_model(setting_name: str) -> ( try: return django_apps.get_model(flag_model_name) except ValueError: - raise ImproperlyConfigured("WAFFLE_{} must be of the form 'app_label.model_name'".format( - setting_name - )) + raise ImproperlyConfigured(f"WAFFLE_{setting_name} must be of the form 'app_label.model_name'") except LookupError: raise ImproperlyConfigured( - "WAFFLE_{} refers to model '{}' that has not been installed".format( - setting_name, flag_model_name - ) + f"WAFFLE_{setting_name} refers to model '{flag_model_name}' that has not been installed" ) diff --git a/waffle/locale/ru/LC_MESSAGES/django.po b/waffle/locale/ru/LC_MESSAGES/django.po index 64a976aa..50fa3cc0 100644 --- a/waffle/locale/ru/LC_MESSAGES/django.po +++ b/waffle/locale/ru/LC_MESSAGES/django.po @@ -2,10 +2,10 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# # Translators: # Clinton Blackburn , 2021 -# +# #, fuzzy msgid "" msgstr "" diff --git a/waffle/management/commands/waffle_delete.py b/waffle/management/commands/waffle_delete.py index dc9e77e5..a9c15663 100644 --- a/waffle/management/commands/waffle_delete.py +++ b/waffle/management/commands/waffle_delete.py @@ -38,7 +38,7 @@ def handle(self, *args: Any, **options: Any) -> None: flag_queryset = get_waffle_flag_model().objects.filter(name__in=flags) flag_count = flag_queryset.count() flag_queryset.delete() - self.stdout.write('Deleted %s Flags' % flag_count) + self.stdout.write(f'Deleted {flag_count} Flags') switches = options['switch_names'] if switches: @@ -47,11 +47,11 @@ def handle(self, *args: Any, **options: Any) -> None: ) switch_count = switches_queryset.count() switches_queryset.delete() - self.stdout.write('Deleted %s Switches' % switch_count) + self.stdout.write(f'Deleted {switch_count} Switches') samples = options['sample_names'] if samples: sample_queryset = get_waffle_sample_model().objects.filter(name__in=samples) sample_count = sample_queryset.count() sample_queryset.delete() - self.stdout.write('Deleted %s Samples' % sample_count) + self.stdout.write(f'Deleted {sample_count} Samples') diff --git a/waffle/management/commands/waffle_flag.py b/waffle/management/commands/waffle_flag.py index 052150bc..9b393ac8 100644 --- a/waffle/management/commands/waffle_flag.py +++ b/waffle/management/commands/waffle_flag.py @@ -104,19 +104,19 @@ def handle(self, *args: Any, **options: Any) -> None: if options['list_flags']: self.stdout.write('Flags:') for flag in get_waffle_flag_model().objects.iterator(): - self.stdout.write('NAME: %s' % flag.name) - self.stdout.write('SUPERUSERS: %s' % flag.superusers) - self.stdout.write('EVERYONE: %s' % flag.everyone) - self.stdout.write('AUTHENTICATED: %s' % flag.authenticated) - self.stdout.write('PERCENT: %s' % flag.percent) - self.stdout.write('TESTING: %s' % flag.testing) - self.stdout.write('ROLLOUT: %s' % flag.rollout) - self.stdout.write('STAFF: %s' % flag.staff) - self.stdout.write('GROUPS: %s' % list( - flag.groups.values_list('name', flat=True)) + self.stdout.write(f'NAME: {flag.name}') + self.stdout.write(f'SUPERUSERS: {flag.superusers}') + self.stdout.write(f'EVERYONE: {flag.everyone}') + self.stdout.write(f'AUTHENTICATED: {flag.authenticated}') + self.stdout.write(f'PERCENT: {flag.percent}') + self.stdout.write(f'TESTING: {flag.testing}') + self.stdout.write(f'ROLLOUT: {flag.rollout}') + self.stdout.write(f'STAFF: {flag.staff}') + self.stdout.write('GROUPS: {}'.format(list( + flag.groups.values_list('name', flat=True))) ) - self.stdout.write('USERS: %s' % list( - flag.users.values_list(UserModel.USERNAME_FIELD, flat=True)) + self.stdout.write('USERS: {}'.format(list( + flag.users.values_list(UserModel.USERNAME_FIELD, flat=True))) ) self.stdout.write('') return @@ -129,7 +129,7 @@ def handle(self, *args: Any, **options: Any) -> None: if options['create']: flag, created = get_waffle_flag_model().objects.get_or_create(name=flag_name) if created: - self.stdout.write('Creating flag: %s' % flag_name) + self.stdout.write(f'Creating flag: {flag_name}') else: try: flag = get_waffle_flag_model().objects.get(name=flag_name) @@ -149,7 +149,7 @@ def handle(self, *args: Any, **options: Any) -> None: group_instance = Group.objects.get(name=group) group_hash[group_instance.name] = group_instance.id except Group.DoesNotExist: - raise CommandError('Group %s does not exist' % group) + raise CommandError(f'Group {group} does not exist') # If 'append' was not passed, we clear related groups if not options['append']: flag.groups.clear() @@ -168,11 +168,11 @@ def handle(self, *args: Any, **options: Any) -> None: ) user_hash.add(user_instance) except UserModel.DoesNotExist: - raise CommandError('User %s does not exist' % username) + raise CommandError(f'User {username} does not exist') # If 'append' was not passed, we clear related users if not options['append']: flag.users.clear() - self.stdout.write('Setting user(s): %s' % user_hash) + self.stdout.write(f'Setting user(s): {user_hash}') # for user in user_hash: flag.users.add(*[user.id for user in user_hash]) elif hasattr(flag, option): diff --git a/waffle/management/commands/waffle_sample.py b/waffle/management/commands/waffle_sample.py index 311887b9..e931917b 100644 --- a/waffle/management/commands/waffle_sample.py +++ b/waffle/management/commands/waffle_sample.py @@ -48,7 +48,7 @@ def handle(self, *args: Any, **options: Any) -> None: try: percent = float(percent) if not (0.0 <= percent <= 100.0): - raise ValueError() + raise ValueError except ValueError: raise CommandError('You need to enter a valid percentage value.') @@ -56,7 +56,7 @@ def handle(self, *args: Any, **options: Any) -> None: sample, created = get_waffle_sample_model().objects.get_or_create( name=sample_name, defaults={'percent': 0}) if created: - self.stdout.write('Creating sample: %s' % sample_name) + self.stdout.write(f'Creating sample: {sample_name}') else: try: sample = get_waffle_sample_model().objects.get(name=sample_name) diff --git a/waffle/management/commands/waffle_switch.py b/waffle/management/commands/waffle_switch.py index 016bba4f..e9d26f45 100644 --- a/waffle/management/commands/waffle_switch.py +++ b/waffle/management/commands/waffle_switch.py @@ -8,8 +8,8 @@ def on_off_bool(string: str) -> bool: if string not in ['on', 'off']: - raise ArgumentTypeError("invalid choice: %r (choose from 'on', " - "'off')" % string) + raise ArgumentTypeError(f"invalid choice: {string!r} (choose from 'on', " + "'off')") return string == 'on' @@ -60,7 +60,7 @@ def handle(self, *args: Any, **options: Any) -> None: name=switch_name ) if created: - self.stdout.write('Creating switch: %s' % switch_name) + self.stdout.write(f'Creating switch: {switch_name}') else: try: switch = get_waffle_switch_model().objects.get(name=switch_name) diff --git a/waffle/templatetags/waffle_tags.py b/waffle/templatetags/waffle_tags.py index 38783f1e..62f5bb7a 100644 --- a/waffle/templatetags/waffle_tags.py +++ b/waffle/templatetags/waffle_tags.py @@ -20,7 +20,7 @@ def __init__(self, nodelist_true, nodelist_false, condition, name, self.compiled_name = compiled_name def __repr__(self): - return '' % self.name + return f'' def __iter__(self): yield from self.nodelist_true @@ -41,16 +41,15 @@ def render(self, context): def handle_token(cls, parser, token, kind, condition): bits = token.split_contents() if len(bits) < 2: - raise template.TemplateSyntaxError("%r tag requires an argument" % - bits[0]) + raise template.TemplateSyntaxError(f"{bits[0]!r} tag requires an argument") name = bits[1] compiled_name = parser.compile_filter(name) - nodelist_true = parser.parse(('else', 'end%s' % kind)) + nodelist_true = parser.parse(('else', f'end{kind}')) token = parser.next_token() if token.contents == 'else': - nodelist_false = parser.parse(('end%s' % kind,)) + nodelist_false = parser.parse((f'end{kind}',)) parser.delete_first_token() else: nodelist_false = template.NodeList() diff --git a/waffle/tests/test_middleware.py b/waffle/tests/test_middleware.py index a37cbc07..11af7c91 100644 --- a/waffle/tests/test_middleware.py +++ b/waffle/tests/test_middleware.py @@ -29,7 +29,7 @@ def test_rollout_cookies(): resp = HttpResponse() resp = WaffleMiddleware().process_response(get, resp) for k in get.waffles: - cookie = 'dwf_%s' % k + cookie = f'dwf_{k}' assert cookie in resp.cookies assert str(get.waffles[k][0]) == resp.cookies[cookie].value if get.waffles[k][1]: @@ -44,6 +44,6 @@ def test_testing_cookies(): resp = HttpResponse() resp = WaffleMiddleware().process_response(get, resp) for k in get.waffle_tests: - cookie = 'dwft_%s' % k + cookie = f'dwft_{k}' assert str(get.waffle_tests[k]) == resp.cookies[cookie].value assert not resp.cookies[cookie]['max-age']