From db7656ba1ce70a1936f883e478e5ffed308bdad1 Mon Sep 17 00:00:00 2001 From: Alex Tomkins Date: Sat, 12 Jul 2025 14:08:31 +0100 Subject: [PATCH 1/6] Django 5.2 for cookiecutter config --- cookiecutter.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter.json b/cookiecutter.json index 8c972f4..76234b4 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,7 +1,7 @@ { "project_name": "Project Name", "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '').replace('-', '').replace('_', '') }}", - "django_version": ["5.0"], + "django_version": ["5.2"], "multilingual": "n", "geodjango": "n", "_copy_without_render": [ From 0a794413516c048c98814fe714a376a5dd357b29 Mon Sep 17 00:00:00 2001 From: Alex Tomkins Date: Sat, 12 Jul 2025 14:11:24 +0100 Subject: [PATCH 2/6] Minimum password length of 12 characters --- .../project/settings/production.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/project/settings/production.py b/{{cookiecutter.project_slug}}/project/settings/production.py index af3be39..f430354 100644 --- a/{{cookiecutter.project_slug}}/project/settings/production.py +++ b/{{cookiecutter.project_slug}}/project/settings/production.py @@ -38,7 +38,12 @@ # Improve password security to a reasonable bare minimum AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + "OPTIONS": { + "min_length": 12, + }, + }, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] From 267d4e376d6ffe35741c77bf95dfc69dbe973180 Mon Sep 17 00:00:00 2001 From: Alex Tomkins Date: Sat, 12 Jul 2025 14:13:01 +0100 Subject: [PATCH 3/6] Updated axes settings --- {{cookiecutter.project_slug}}/project/settings/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/project/settings/base.py b/{{cookiecutter.project_slug}}/project/settings/base.py index 8c7298d..3f6ec3d 100644 --- a/{{cookiecutter.project_slug}}/project/settings/base.py +++ b/{{cookiecutter.project_slug}}/project/settings/base.py @@ -239,15 +239,16 @@ # Improved login security with axes # - Only lock attempts by username (prevent mass attempts on single accounts) -# - 10 failures in 15 attempts results in blocking +# - 10 failures in 5 minutes results in blocking AUTHENTICATION_BACKENDS = [ "axes.backends.AxesBackend", "django.contrib.auth.backends.ModelBackend", ] AXES_LOCKOUT_PARAMETERS = ["username"] AXES_FAILURE_LIMIT = 10 -AXES_COOLOFF_TIME = timedelta(minutes=15) +AXES_COOLOFF_TIME = timedelta(minutes=5) AXES_ENABLE_ADMIN = False +AXES_SENSITIVE_PARAMETERS = [] {%- if cookiecutter.geodjango == 'y' %} # GeoDjango fixes From d04de936384d57b1a6bd29e5688f766b2166a0e9 Mon Sep 17 00:00:00 2001 From: Alex Tomkins Date: Sat, 12 Jul 2025 14:13:59 +0100 Subject: [PATCH 4/6] No test-a11y --- {{cookiecutter.project_slug}}/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/package.json b/{{cookiecutter.project_slug}}/package.json index 991c569..9bcf3f6 100644 --- a/{{cookiecutter.project_slug}}/package.json +++ b/{{cookiecutter.project_slug}}/package.json @@ -39,7 +39,6 @@ "prettier": "prettier", "stylelint": "stylelint", "start": "webpack --config webpack.config.js --config-name 'development' --mode development --watch", - "production": "webpack --config webpack.config.js --config-name 'production' --mode production", - "test-a11y": "node ./tests/src/pa11y.mjs" + "production": "webpack --config webpack.config.js --config-name 'production' --mode production" } } From f4757ed5ff267fcf1a5d404836f7546b3676c028 Mon Sep 17 00:00:00 2001 From: Alex Tomkins Date: Sat, 12 Jul 2025 14:23:24 +0100 Subject: [PATCH 5/6] psycopg-c for production --- {{cookiecutter.project_slug}}/requirements/production.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index a3e81b8..004f582 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -1 +1,3 @@ -r base.txt + +psycopg-c==3.2.9 From f8286b8e7aaa4d975937243214a9b0fb3eb58392 Mon Sep 17 00:00:00 2001 From: Alex Tomkins Date: Sat, 12 Jul 2025 14:23:38 +0100 Subject: [PATCH 6/6] Update ruff --- .github/workflows/matchers/ruff.json | 14 ++ .../.github/workflows/ci_geodjango.yml | 4 +- .../.github/workflows/ci_standard.yml | 4 +- {{cookiecutter.project_slug}}/Makefile | 40 +--- .../apps/accounts/tests/factories.py | 4 +- .../apps/core/context_processors.py | 2 +- .../core/management/commands/runserver.py | 8 +- {{cookiecutter.project_slug}}/fabfile.py | 194 +++++++++--------- {{cookiecutter.project_slug}}/manage.py | 3 +- .../project/settings/demo.py | 2 +- .../project/settings/local.py | 7 +- .../project/settings/migrations.py | 4 +- .../project/settings/production.py | 5 +- .../project/settings/tox.py | 7 +- {{cookiecutter.project_slug}}/pyproject.toml | 118 +++++++---- .../requirements/local.txt | 1 + .../requirements/testing.txt | 5 +- 17 files changed, 235 insertions(+), 187 deletions(-) create mode 100644 .github/workflows/matchers/ruff.json diff --git a/.github/workflows/matchers/ruff.json b/.github/workflows/matchers/ruff.json new file mode 100644 index 0000000..a9a5917 --- /dev/null +++ b/.github/workflows/matchers/ruff.json @@ -0,0 +1,14 @@ +{ + "problemMatcher": [ + { + "owner": "ruff", + "pattern": [ + { + "regexp": "^(Would reformat): (.+)$", + "message": 1, + "file": 2 + } + ] + } + ] +} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci_geodjango.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci_geodjango.yml index be5261f..c4c4366 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/ci_geodjango.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci_geodjango.yml @@ -50,9 +50,11 @@ jobs: PGHOST: localhost PGUSER: postgres PGPASSWORD: password - TOX_OVERRIDE: "testenv.pass_env=PG*" + RUFF_OUTPUT_FORMAT: github + TOX_OVERRIDE: "testenv.pass_env=PG*,RUFF_OUTPUT_FORMAT" run: | pip install $(grep -E "^(tox|tox-uv)==" requirements/local.txt) + echo "::add-matcher::.github/workflows/matchers/ruff.json" tox services: postgres: diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci_standard.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci_standard.yml index 2dd78d6..1dd9b4f 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/ci_standard.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci_standard.yml @@ -45,9 +45,11 @@ jobs: PGHOST: localhost PGUSER: postgres PGPASSWORD: password - TOX_OVERRIDE: "testenv.pass_env=PG*" + RUFF_OUTPUT_FORMAT: github + TOX_OVERRIDE: "testenv.pass_env=PG*,RUFF_OUTPUT_FORMAT" run: | pip install $(grep -E "^(tox|tox-uv)==" requirements/local.txt) + echo "::add-matcher::.github/workflows/matchers/ruff.json" tox services: postgres: diff --git a/{{cookiecutter.project_slug}}/Makefile b/{{cookiecutter.project_slug}}/Makefile index 1aa5207..59e5f8c 100644 --- a/{{cookiecutter.project_slug}}/Makefile +++ b/{{cookiecutter.project_slug}}/Makefile @@ -51,10 +51,10 @@ check: ## Check for any obvious errors in the project's setup. check: pipdeptree-check npm-check django-check format: ## Run this project's code formatters. -format: ruff-format black-format isort-format prettier-format stylelint-format djlint-format +format: ruff-format prettier-format stylelint-format djlint-format lint: ## Lint the project. -lint: ruff-lint black-lint isort-lint flake8-lint npm-install eslint-lint prettier-lint stylelint-lint djlint-lint djlint-check +lint: ruff-lint eslint-lint prettier-lint stylelint-lint djlint-lint djlint-check test: ## Run unit and integration tests. test: django-test @@ -68,8 +68,6 @@ test-report: coverage-clean test coverage-report deploy: ## Deploy this project to demo or live. deploy: fab-deploy - - # --------------- # Utility targets # --------------- @@ -119,28 +117,6 @@ fab-deploy: fab deploy -# Ruff -ruff-lint: - ruff check . - ruff format --check apps project - -ruff-format: - ruff check --fix-only . - - -# ISort -isort-lint: - isort --check-only --diff apps project - -isort-format: - isort apps project - - -# Flake8 -flake8-lint: - flake8 apps project - - # Coverage coverage-report: coverage-html coverage-xml coverage report --show-missing @@ -225,12 +201,14 @@ stylelint-format: npm run stylelint --silent -- static/src/scss --fix -# Black -black-lint: - black --check apps project +# Ruff +ruff-lint: + ruff check + ruff format --check -black-format: - black apps project +ruff-format: + ruff check --fix-only + ruff format # DJ lint diff --git a/{{cookiecutter.project_slug}}/apps/accounts/tests/factories.py b/{{cookiecutter.project_slug}}/apps/accounts/tests/factories.py index afc1fd9..b6f90d3 100644 --- a/{{cookiecutter.project_slug}}/apps/accounts/tests/factories.py +++ b/{{cookiecutter.project_slug}}/apps/accounts/tests/factories.py @@ -7,8 +7,8 @@ class UserFactory(factory.django.DjangoModelFactory): username = factory.Sequence(lambda n: f"user{n}") first_name = factory.Faker("first_name") last_name = factory.Faker("last_name") - email = factory.Sequence(lambda n: "person{0}@example.org".format(n)) - password = "test123" + email = factory.Sequence(lambda n: f"person{n}@example.org") + password = "test123" # noqa:S105 class Meta: model = User diff --git a/{{cookiecutter.project_slug}}/apps/core/context_processors.py b/{{cookiecutter.project_slug}}/apps/core/context_processors.py index c7c2324..4d246ca 100644 --- a/{{cookiecutter.project_slug}}/apps/core/context_processors.py +++ b/{{cookiecutter.project_slug}}/apps/core/context_processors.py @@ -7,7 +7,7 @@ BROWSERSYNC_URL = "http://{host}:{port}/browser-sync/browser-sync-client.js?t={time}" -@functools.lru_cache() +@functools.cache def browsersync_url(host): """ Return the browsersync javascript URL for a given hostname, or None if disabled. diff --git a/{{cookiecutter.project_slug}}/apps/core/management/commands/runserver.py b/{{cookiecutter.project_slug}}/apps/core/management/commands/runserver.py index b071813..065d580 100644 --- a/{{cookiecutter.project_slug}}/apps/core/management/commands/runserver.py +++ b/{{cookiecutter.project_slug}}/apps/core/management/commands/runserver.py @@ -61,17 +61,17 @@ def start_webpack(self, **options): webpack_args.append("--display=errors-only") if webpack_args: - webpack_args = ["--"] + webpack_args + webpack_args = ["--", *webpack_args] self.stdout.write(">>> Starting webpack") - self.webpack_process = subprocess.Popen( - ["npm", "start"] + webpack_args, + self.webpack_process = subprocess.Popen( # noqa:S603 + ["npm", "start", *webpack_args], # noqa:S607 shell=False, stdin=subprocess.PIPE, stdout=self.stdout._out, stderr=self.stderr._out, ) - self.stdout.write(">>> Webpack process on pid {}".format(self.webpack_process.pid)) + self.stdout.write(f">>> Webpack process on pid {self.webpack_process.pid}") def kill_webpack_process(): self.stdout.write(">>> Closing webpack process") diff --git a/{{cookiecutter.project_slug}}/fabfile.py b/{{cookiecutter.project_slug}}/fabfile.py index 9988c64..83e7fab 100644 --- a/{{cookiecutter.project_slug}}/fabfile.py +++ b/{{cookiecutter.project_slug}}/fabfile.py @@ -1,3 +1,4 @@ +# ruff: noqa import random from fabric.api import cd, env, execute, local, parallel, roles, run, runs_once, task @@ -5,25 +6,25 @@ # Changable settings env.roledefs = { - 'web': [ - '{{ cookiecutter.project_slug }}@scorch.devsoc.org', - '{{ cookiecutter.project_slug }}@smaug.devsoc.org', + "web": [ + "{{ cookiecutter.project_slug }}@scorch.devsoc.org", + "{{ cookiecutter.project_slug }}@smaug.devsoc.org", ], - 'demo': [ - '{{ cookiecutter.project_slug }}@trogdor.devsoc.org', + "demo": [ + "{{ cookiecutter.project_slug }}@trogdor.devsoc.org", ], - 'cron': [ - '{{ cookiecutter.project_slug }}@{{ ["scorch", "smaug"]|random() }}.devsoc.org', + "cron": [ + "{{ cookiecutter.project_slug }}@{{ ["scorch", "smaug"]|random() }}.devsoc.org", ], } -env.home = env.get('home', '/var/www/{{ cookiecutter.project_slug }}') -env.virtualenv = env.get('virtualenv', '/var/envs/{{ cookiecutter.project_slug }}') -env.appname = env.get('appname', '{{ cookiecutter.project_slug }}') -env.repo = env.get('repo', '{{ cookiecutter.project_slug }}') -env.media = env.get('media', '{{ cookiecutter.project_slug }}') -env.media_bucket = env.get('media_bucket', 'contentfiles-media-eu-west-1') -env.database = env.get('database', '{{ cookiecutter.project_slug }}_django') +env.home = env.get("home", "/var/www/{{ cookiecutter.project_slug }}") +env.virtualenv = env.get("virtualenv", "/var/envs/{{ cookiecutter.project_slug }}") +env.appname = env.get("appname", "{{ cookiecutter.project_slug }}") +env.repo = env.get("repo", "{{ cookiecutter.project_slug }}") +env.media = env.get("media", "{{ cookiecutter.project_slug }}") +env.media_bucket = env.get("media_bucket", "contentfiles-media-eu-west-1") +env.database = env.get("database", "{{ cookiecutter.project_slug }}_django") CRONTAB = """ MAILTO="" @@ -33,18 +34,18 @@ # Avoid tweaking these env.use_ssh_config = True -GIT_REMOTE = 'git@github.com:developersociety/{env.repo}.git' +GIT_REMOTE = "git@github.com:developersociety/{env.repo}.git" @task def demo(): - env.roledefs['web'] = env.roledefs['demo'] - env.roledefs['cron'] = env.roledefs['demo'] - env.media_bucket = 'contentfiles-demo-media-eu-west-1' + env.roledefs["web"] = env.roledefs["demo"] + env.roledefs["cron"] = env.roledefs["demo"] + env.media_bucket = "contentfiles-demo-media-eu-west-1" @task -@roles('cron') +@roles("cron") def cron(remove=None): """ Crontab setup. @@ -56,20 +57,21 @@ def cron(remove=None): """ # Allow quick removal if needed if remove: - run('crontab -r') + run("crontab -r") return # Deterministic based on hostname random.seed(env.host_string) # Several templates - can add more if needed - def every_x(minutes=60, hour='*', day='*', month='*', day_of_week='*'): + def every_x(minutes=60, hour="*", day="*", month="*", day_of_week="*"): # Add some randomness to minutes start = random.randint(0, minutes - 1) - minute = ','.join(str(x) for x in range(start, 60, minutes)) + minute = ",".join(str(x) for x in range(start, 60, minutes)) - return '{minute} {hour} {day} {month} {day_of_week}'.format( - minute=minute, hour=hour, day=day, month=month, day_of_week=day_of_week) + return "{minute} {hour} {day} {month} {day_of_week}".format( + minute=minute, hour=hour, day=day, month=month, day_of_week=day_of_week + ) cron = CRONTAB.format( axes_reset_logs=every_x(hour=random.randint(0, 23)), @@ -79,9 +81,9 @@ def every_x(minutes=60, hour='*', day='*', month='*', day_of_week='*'): @task -@roles('web') +@roles("web") @parallel -def clone_repo(branch='main'): +def clone_repo(branch="main"): """ Initial site setup. @@ -91,47 +93,44 @@ def clone_repo(branch='main'): fab clone_repo:branchname """ with cd(env.home): - if not exists('.git'): + if not exists(".git"): git_repo = GIT_REMOTE.format(env=env) - run('git clone --quiet --recursive {} .'.format(git_repo)) + run("git clone --quiet --recursive {} .".format(git_repo)) else: - run('git fetch') + run("git fetch") - run('git checkout {}'.format(branch)) + run("git checkout {}".format(branch)) @task -@roles('web') +@roles("web") @parallel def update(): - """ Pull latest git repository changes and install requirements. """ + """Pull latest git repository changes and install requirements.""" with cd(env.home): - run('git pull') + run("git pull") # Save the current git commit for Sentry release tracking - run('git rev-parse HEAD > .sentry-release') + run("git rev-parse HEAD > .sentry-release") # Install python packages - run('pip install --quiet --requirement requirements/production.txt') + run("pip install --quiet --requirement requirements/production.txt") # Install nvm using .nvmrc version - run('nvm install --default --no-progress') + run("nvm install --default --no-progress") # Check for changes in nvm or package-lock.json + run("cmp --silent .nvmrc node_modules/.nvmrc || rm -f node_modules/.package-lock.json") run( - 'cmp --silent .nvmrc node_modules/.nvmrc || ' - 'rm -f node_modules/.package-lock.json' - ) - run( - 'cmp --silent package-lock.json node_modules/.package-lock.json || ' - 'rm -f node_modules/.package-lock.json' + "cmp --silent package-lock.json node_modules/.package-lock.json || " + "rm -f node_modules/.package-lock.json" ) # Install node packages - if not exists('node_modules/.package-lock.json'): - run('npm ci --no-progress') - run('cp -a package-lock.json node_modules/.package-lock.json') - run('cp -a .nvmrc node_modules/.nvmrc') + if not exists("node_modules/.package-lock.json"): + run("npm ci --no-progress") + run("cp -a package-lock.json node_modules/.package-lock.json") + run("cp -a .nvmrc node_modules/.nvmrc") # Clean up any potential cruft run('find -name "__pycache__" -prune -exec rm -rf {} \;') @@ -139,20 +138,21 @@ def update(): @task @runs_once -@roles('web') +@roles("web") def migrate(fake=False): - """ Migrate database changes. """ + """Migrate database changes.""" with cd(env.home): if fake: - run('python manage.py migrate --fake') + run("python manage.py migrate --fake") else: - run('python manage.py migrate') + run("python manage.py migrate") + @task @runs_once -@roles('web') +@roles("web") def truncate_migrations_table(): - """ Wipe out migrations table - used as part of reset_migrations. """ + """Wipe out migrations table - used as part of reset_migrations.""" with cd(env.home): run('echo "TRUNCATE django_migrations;" | python manage.py dbshell') @@ -172,35 +172,35 @@ def reset_migrations(): @task -@roles('web') +@roles("web") @parallel def static(): - """ Update static files. """ + """Update static files.""" with cd(env.home): # Clean dist folder - run('rm -rf static/dist') + run("rm -rf static/dist") # Generate CSS - run('npm run production --silent') + run("npm run production --silent") # Collect static files - run('python manage.py collectstatic --verbosity=0 --noinput') + run("python manage.py collectstatic --verbosity=0 --noinput") {%- if cookiecutter.multilingual == 'y' %} @task -@roles('web') +@roles("web") @parallel def translations(): - """ Update translation files. """ - with cd(env.home), cd('locale'): - run('python ../manage.py compilemessages --verbosity=0') + """Update translation files.""" + with cd(env.home), cd("locale"): + run("python ../manage.py compilemessages --verbosity=0") {%- endif %} -@task(name='reload') -@roles('web') +@task(name="reload") +@roles("web") @parallel def reload_uwsgi(force_reload=None): """ @@ -211,25 +211,29 @@ def reload_uwsgi(force_reload=None): fab reload:force_reload=True """ if force_reload: - run('uwsgi --stop /run/uwsgi/{}/uwsgi.pid'.format(env.appname)) + run("uwsgi --stop /run/uwsgi/{}/uwsgi.pid".format(env.appname)) else: - run('uwsgi --reload /run/uwsgi/{}/uwsgi.pid'.format(env.appname)) + run("uwsgi --reload /run/uwsgi/{}/uwsgi.pid".format(env.appname)) @task -@roles('web') +@roles("web") @runs_once def sentry_release(): - """ Register new release with Sentry. """ + """Register new release with Sentry.""" with cd(env.home): - version = run('sentry-cli releases propose-version') - run('sentry-cli releases new --finalize --project {project} {version}'.format( - project=env.repo, version=version - )) - run('sentry-cli releases set-commits --auto {version}'.format(version=version)) - run('sentry-cli releases deploys new --release {version} --env $SENTRY_ENVIRONMENT'.format( - version=version - )) + version = run("sentry-cli releases propose-version") + run( + "sentry-cli releases new --finalize --project {project} {version}".format( + project=env.repo, version=version + ) + ) + run("sentry-cli releases set-commits --auto {version}".format(version=version)) + run( + "sentry-cli releases deploys new --release {version} --env $SENTRY_ENVIRONMENT".format( + version=version + ) + ) @task @@ -255,7 +259,7 @@ def deploy(force_reload=None): @task -def get_backup(hostname=None, replace_hostname='127.0.0.1', replace_port=8000): +def get_backup(hostname=None, replace_hostname="127.0.0.1", replace_port=8000): """ Get remote backup and restore database locally. @@ -265,11 +269,11 @@ def get_backup(hostname=None, replace_hostname='127.0.0.1', replace_port=8000): fab get_backup:hostname=www.example.com,replace_hostname=192.1.1.1,replace_port=8000 """ # Recreate database - local('dropdb --if-exists {}'.format(env.database)) - local('createdb {}'.format(env.database)) + local("dropdb --if-exists {}".format(env.database)) + local("createdb {}".format(env.database)) # Connect to the server and dump database. - backup_ssh = random.choice(env.roledefs['web']) + backup_ssh = random.choice(env.roledefs["web"]) commands = [ """ ssh -C {} '( @@ -278,13 +282,15 @@ def get_backup(hostname=None, replace_hostname='127.0.0.1', replace_port=8000): && {}/manage.py dump_masked_data )' """.format( - backup_ssh, env.virtualenv, env.home, + backup_ssh, + env.virtualenv, + env.home, ).strip() ] if hostname: if replace_port: - replace_host = '{}:{}'.format(replace_hostname, replace_port) + replace_host = "{}:{}".format(replace_hostname, replace_port) else: replace_host = replace_hostname @@ -292,13 +298,13 @@ def get_backup(hostname=None, replace_hostname='127.0.0.1', replace_port=8000): commands.append('sed -e "s|{}|{}|g"'.format(hostname, replace_host)) # Restore database. - commands.append('psql --single-transaction {}'.format(env.database)) + commands.append("psql --single-transaction {}".format(env.database)) - local(' | '.join(commands)) + local(" | ".join(commands)) @task -def get_media(directory='', filetype=None): +def get_media(directory="", filetype=None): """ Download remote media files. It uses credentials from ~/.aws/config. @@ -309,10 +315,10 @@ def get_media(directory='', filetype=None): params = '--exclude "*" --include "*.{ext}"'.format(ext=filetype) if filetype else "" # Sync files from our S3 bucket/directory local( - 'aws-vault exec devsoc-contentfiles-download -- ' - 'aws s3 sync ' - 's3://{media_bucket}/{media}/{directory} ' - 'htdocs/media/{directory} {params}'.format( + "aws-vault exec devsoc-contentfiles-download -- " + "aws s3 sync " + "s3://{media_bucket}/{media}/{directory} " + "htdocs/media/{directory} {params}".format( media_bucket=env.media_bucket, media=env.media, directory=directory, params=params ) ) @@ -320,16 +326,16 @@ def get_media(directory='', filetype=None): @task @runs_once -@roles('web') +@roles("web") def get_env(): - """ Get the current environment variables, ready for local export.""" + """Get the current environment variables, ready for local export.""" env.output_prefix = False run('export | sed -e "s/declare -x/export/g"') @task -def ssh(index='1'): - """ SSH to the remote server, pass ssh:2 to go to the second defined. """ +def ssh(index="1"): + """SSH to the remote server, pass ssh:2 to go to the second defined.""" index = int(index) - 1 - server = env.roledefs['web'][index] - local('ssh {}'.format(server)) + server = env.roledefs["web"][index] + local("ssh {}".format(server)) diff --git a/{{cookiecutter.project_slug}}/manage.py b/{{cookiecutter.project_slug}}/manage.py index cb2975a..bec1fcf 100755 --- a/{{cookiecutter.project_slug}}/manage.py +++ b/{{cookiecutter.project_slug}}/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys @@ -8,7 +9,7 @@ def main(): """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.local") try: - from django.core.management import execute_from_command_line + from django.core.management import execute_from_command_line # noqa:PLC0415 except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " diff --git a/{{cookiecutter.project_slug}}/project/settings/demo.py b/{{cookiecutter.project_slug}}/project/settings/demo.py index 8331477..7a4216b 100644 --- a/{{cookiecutter.project_slug}}/project/settings/demo.py +++ b/{{cookiecutter.project_slug}}/project/settings/demo.py @@ -1,4 +1,4 @@ -from .production import * # noqa +from .production import * # noqa:F403 EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" diff --git a/{{cookiecutter.project_slug}}/project/settings/local.py b/{{cookiecutter.project_slug}}/project/settings/local.py index 2b6940d..e4eff01 100644 --- a/{{cookiecutter.project_slug}}/project/settings/local.py +++ b/{{cookiecutter.project_slug}}/project/settings/local.py @@ -1,7 +1,8 @@ +# ruff: noqa:F405 import os import sys -from .base import * # noqa +from .base import * # noqa:F403 DEBUG = True TEMPLATES[0]["OPTIONS"]["debug"] = True @@ -30,7 +31,7 @@ # Use vanilla StaticFilesStorage to allow tests to run outside of tox easily STORAGES["staticfiles"]["BACKEND"] = "django.contrib.staticfiles.storage.StaticFilesStorage" -SECRET_KEY = "secret" +SECRET_KEY = "secret" # noqa:S105 EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" @@ -39,7 +40,7 @@ if not os.environ.get("DISABLE_TOOLBAR") and "test" not in sys.argv: INSTALLED_APPS += ["debug_toolbar"] - MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE + MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE] DEBUG_TOOLBAR_CONFIG = { "SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/", "bootstrap/"), diff --git a/{{cookiecutter.project_slug}}/project/settings/migrations.py b/{{cookiecutter.project_slug}}/project/settings/migrations.py index 4883d94..112c0eb 100644 --- a/{{cookiecutter.project_slug}}/project/settings/migrations.py +++ b/{{cookiecutter.project_slug}}/project/settings/migrations.py @@ -1,7 +1,7 @@ -from .base import * # noqa +from .base import * # noqa:F403 # makemigrations --check requires a database if DATABASES is populated, but works fine without, so # we set this to an empty dict to stop makemigrations connecting to a database which doesn't exist DATABASES = {} -SECRET_KEY = "secret" +SECRET_KEY = "secret" # noqa:S105 diff --git a/{{cookiecutter.project_slug}}/project/settings/production.py b/{{cookiecutter.project_slug}}/project/settings/production.py index f430354..3a751c0 100644 --- a/{{cookiecutter.project_slug}}/project/settings/production.py +++ b/{{cookiecutter.project_slug}}/project/settings/production.py @@ -1,3 +1,4 @@ +# ruff: noqa:F405 import os from pathlib import Path @@ -5,7 +6,7 @@ from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import ignore_logger -from .base import * # noqa +from .base import * # noqa:F403 ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(" ") @@ -63,7 +64,7 @@ if os.environ.get("ELASTIC_APM_SERVER_URL"): INSTALLED_APPS += ["elasticapm.contrib.django.apps.ElasticAPMConfig"] - MIDDLEWARE = ["elasticapm.contrib.django.middleware.TracingMiddleware"] + MIDDLEWARE + MIDDLEWARE = ["elasticapm.contrib.django.middleware.TracingMiddleware", *MIDDLEWARE] # Cache sessions for optimum performance if os.environ.get("REDIS_SERVERS"): diff --git a/{{cookiecutter.project_slug}}/project/settings/tox.py b/{{cookiecutter.project_slug}}/project/settings/tox.py index 278dc1a..87e8726 100644 --- a/{{cookiecutter.project_slug}}/project/settings/tox.py +++ b/{{cookiecutter.project_slug}}/project/settings/tox.py @@ -1,6 +1,9 @@ +# ruff: noqa:F405 +import os + import dj_database_url -from .base import * # noqa +from .base import * # noqa:F403 # Tests are performed on a test_ database, however to avoid any connections/queries going to # another database we also set this as the 'default' as well @@ -12,7 +15,7 @@ {%- endif %} DATABASES["default"]["TEST"] = {"NAME": DATABASES["default"]["NAME"]} -SECRET_KEY = "secret" +SECRET_KEY = "secret" # noqa:S105 STATIC_ROOT = os.environ["STATIC_ROOT"] diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index a9336cc..494b7a5 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -1,19 +1,6 @@ [tool.black] line-length = 99 -target-version = ['py312'] -exclude = '/migrations/' - -[tool.isort] -combine_as_imports = true -sections = ['FUTURE','STDLIB','DJANGO','THIRDPARTY','FIRSTPARTY','LOCALFOLDER'] -known_django = 'django' -known_first_party = 'apps/' -include_trailing_comma = true -float_to_top = true -force_grid_wrap = 0 -line_length = 99 -multi_line_output = 3 -skip_glob = '*/migrations/*.py' +target-version = ["py312"] [tool.djlint] profile = "django" @@ -39,33 +26,88 @@ extend_exclude="field.html" [tool.ruff] extend-exclude = ["apps/*/migrations"] +src = ["apps"] line-length = 99 +target-version = "py312" [tool.ruff.lint] +exclude = ["apps/*/migrations"] +extend-select = [ + "ERA", # eradicate + "YTT", # flake8-2020 + "ASYNC", # flake8-async + "S", # flake8-bandit + "BLE", # flake8-blind-except + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "LOG", # flake8-logging + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLOT", # flake8-slots + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TD", # flake8-todos + "TCH", # flake8-type-checking + "PTH", # flake8-use-pathlib + "FLY", # flynt + "I", # isort + "NPY", # numpy-specific rules + "PD", # pandas-vet + "N", # pep8-naming + "PERF", # perflint + "E", # pycodestyle + "W", # pycodestyle + "F", # pyflakes + "PGH", # pygrep-hooks + "PLC", # pylint + "PLE", # pylint + "PLW", # pylint + "UP", # pyupgrade + "FURB", # refurb + "RUF", # ruff-specific rules + "TRY", # tryceratops +] ignore = [ - "F405", - "W605", - "N802", # Ignore lowercase function name rule- conflicts with assertEqual etc. + "COM812", # flake8-commas: missing-trailing-comma + "EM101", # flake8-errmsg: raw-string-in-exception + "ISC001", # flake8-implicit-str-concat: single-line-implicit-string-concatenation + "RUF012", # ruff-specific rules: mutable-class-default + "SIM105", # flake8-simplify: suppressible-exception + "SIM108", # flake8-simplify: if-else-block-instead-of-if-exp + "TD002", # flake8-todos: missing-todo-author + "TRY003", # tryceratops: raise-vanilla-args ] -# Adds additional rules to the default "E" (pycodestyle) and "F" (pyflakes) -extend-select = [ - "W", # pycodestyle Warning, see https://docs.astral.sh/ruff/rules/#warning-w - "N", # pep8-naming, see https://docs.astral.sh/ruff/rules/#pep8-naming-n - "YTT", # flake8-2020 flake8 plugin which checks for misuse of sys.version or sys.version_info, see https://docs.astral.sh/ruff/rules/#flake8-2020-ytt - "BLE", # checks for blind, catch all except and except Exception statements, see https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble - "B", # flake8-bugbear, see https://docs.astral.sh/ruff/rules/#flake8-bugbear-b - "A", # checks for variables that use Python builtin names, see https://docs.astral.sh/ruff/rules/#flake8-builtins-a - "T100", # Check for pdb;idbp imports and set traces, see https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 - "DJ001", # Avoid using null=True on string-based fields such as CharField and TextField, see https://docs.astral.sh/ruff/rules/django-nullable-model-string-field/ - "DJ003", # Avoid passing locals() as context to a render function, see https://docs.astral.sh/ruff/rules/django-locals-in-render-function/ - "DJ012", # Check that the order of model's inner classes, methods, and fields, see https://docs.astral.sh/ruff/rules/django-unordered-body-content-in-model/ - "ISC", # looks for style problems like implicitly concatenated string literals on the same line, see https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc - "INP001", # Checks for packages that are missing an __init__.py file, see https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp - "PIE", # flake8-pie, multiple tools, see https://docs.astral.sh/ruff/rules/#flake8-pie-pie - "T20", # removes print and pprint statements, see https://docs.astral.sh/ruff/rules/#flake8-print-t20 - "RET", # multiple rules, see https://docs.astral.sh/ruff/rules/#flake8-return-ret - "SIM107", # Checks for return statements in try-except and finally blocks, see https://docs.astral.sh/ruff/rules/return-in-try-except-finally/ - "SIM114", # Checks for if branches with identical arm bodies, see https://docs.astral.sh/ruff/rules/if-with-same-arms/ - "SIM115", # Checks for usages of the builtin open() function without an associated context manager, see https://docs.astral.sh/ruff/rules/open-file-with-context-handler/ - "PD", # Pandas best practices, see https://docs.astral.sh/ruff/rules/#pandas-vet-pd + +[tool.ruff.lint.per-file-ignores] +"fabfile.py" = [ + "PGH004", # pygrep-hooks: blanket-noqa +] + +[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"] + +[tool.ruff.lint.pep8-naming] +extend-ignore-names = [ + "assert*", ] diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index 9da46a2..defe07e 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -1,5 +1,6 @@ -r testing.txt +black==25.1.0 django-debug-toolbar==5.2.0 ipdb==0.13.13 pywatchman==3.0.0 diff --git a/{{cookiecutter.project_slug}}/requirements/testing.txt b/{{cookiecutter.project_slug}}/requirements/testing.txt index 6285b7a..7a78749 100644 --- a/{{cookiecutter.project_slug}}/requirements/testing.txt +++ b/{{cookiecutter.project_slug}}/requirements/testing.txt @@ -1,13 +1,10 @@ -r base.txt -black==24.4.1 coverage==7.9.2 django-extensions==4.1 djlint==1.36.4 factory-boy==3.3.3 -flake8==7.0.0 -isort==5.13.2 pipdeptree==2.27.0 -ruff==0.4.1 +ruff==0.12.3 tblib==3.1.0 unittest-xml-reporting==3.2.0