From 527684cfd5bd9a796f8b566bf6ca7105bfe656cf Mon Sep 17 00:00:00 2001 From: Hugo Rodger-Brown Date: Mon, 13 Nov 2023 13:28:03 +0000 Subject: [PATCH] Add support for Django 5.0, drop <3.2 --- .flake8 | 38 ----- .github/workflows/tox.yml | 59 ++++++-- .isort.cfg | 8 -- .pre-commit-config.yaml | 67 +++------ .ruff.toml | 66 +++++++++ README.md | 133 ++++++++++-------- magic_link/__init__.py | 1 - magic_link/admin.py | 2 - magic_link/apps.py | 2 +- magic_link/migrations/0001_initial.py | 1 - .../migrations/0002_magiclink_logged_in_at.py | 1 - .../migrations/0003_magiclink_accessed_at.py | 1 - .../0004_remove_magiclinkuse_link_is_valid.py | 1 - pyproject.toml | 28 ++-- tests/migrations/0001_initial.py | 1 - tests/settings.py | 1 + tox.ini | 43 +++--- 17 files changed, 253 insertions(+), 200 deletions(-) delete mode 100644 .flake8 delete mode 100644 .isort.cfg create mode 100644 .ruff.toml diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 1abb1ca..0000000 --- a/.flake8 +++ /dev/null @@ -1,38 +0,0 @@ -[flake8] -max-line-length = 88 -max-complexity = 8 -# http://flake8.pycqa.org/en/2.5.5/warnings.html#warning-error-codes -ignore = - # pydocstyle - docstring conventions (PEP257) - D100 # Missing docstring in public module - D101 # Missing docstring in public class - D102 # Missing docstring in public method - D103 # Missing docstring in public function - D104 # Missing docstring in public package - D105 # Missing docstring in magic method - D106 # Missing docstring in public nested class - D107 # Missing docstring in __init__ - D412 # No blank lines allowed between a section header and its content - # pycodestyle - style checker (PEP8) - W503 # line break before binary operator - # the following are ignored in CI using --extend-ignore option: - ; D205 # [pydocstyle] 1 blank line required between summary line and description - ; D400 # [pydocstyle] First line should end with a period - ; D401 # [pydocstyle] First line should be in imperative mood - ; S308 # [bandit] Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed. - ; S703 # [bandit] Potential XSS on mark_safe function. - -per-file-ignores = - ; D205 - 1 blank line required between summary line and description - ; D400 - First line should end with a period - ; D401 - First line should be in imperative mood - ; S101 - use of assert - ; S106 - hard-coded password - ; E501 - line-length - ; E731 - assigning a lambda to a variable - *tests/*:D205,D400,D401,S101,S106,E501,E731 - */migrations/*:E501 - ; F403 - unable to detect undefined names - ; F405 - may be undefined, or defined from star imports - */settings.py:F403,F405 - */settings/*:F403,F405 diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 8ba358c..9497167 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -14,18 +14,41 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toxenv: [fmt,lint,mypy] + toxenv: [fmt, lint, mypy] env: TOXENV: ${{ matrix.toxenv }} steps: - name: Check out the repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Set up Python 3.9 - uses: actions/setup-python@v1 + - name: Set up Python (3.11) + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.11" + + - name: Install and run tox + run: | + pip install tox + tox + + checks: + name: Run Django checks + runs-on: ubuntu-latest + strategy: + matrix: + toxenv: ["django-checks"] + env: + TOXENV: ${{ matrix.toxenv }} + + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up Python (3.11) + uses: actions/setup-python@v4 + with: + python-version: "3.11" - name: Install and run tox run: | @@ -37,18 +60,34 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [3.9] - django: [22,32,main] + python: ["3.8", "3.9", "3.10", "3.11", "3.12"] + # build LTS version, next version, HEAD + django: ["32", "42", "50", "main"] + exclude: + - python: "3.8" + django: "50" + - python: "3.8" + django: "main" + - python: "3.9" + django: "50" + - python: "3.9" + django: "main" + - python: "3.10" + django: "main" + - python: "3.11" + django: "32" + - python: "3.12" + django: "32" env: - TOXENV: py${{ matrix.python }}-django${{ matrix.django }} + TOXENV: django${{ matrix.django }}-py${{ matrix.python }} steps: - name: Check out the repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index a32eb71..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[settings] -default_section=THIRDPARTY -indent=' ' -sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER -multi_line_output=3 -line_length=88 -include_trailing_comma=True -use_parentheses=True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ced139..def9258 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,47 +1,26 @@ repos: - # python import sorting - will amend files - - repo: https://github.com/pre-commit/mirrors-isort - rev: v5.9.3 - hooks: - - id: isort + # python code formatting - will amend files + - repo: https://github.com/ambv/black + rev: 23.10.1 + hooks: + - id: black - # python code formatting - will amend files - - repo: https://github.com/ambv/black - rev: 21.8b0 - hooks: - - id: black + - repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: "v0.1.5" + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 - hooks: - - id: pyupgrade - - # Flake8 includes pyflakes, pycodestyle, mccabe, pydocstyle, bandit - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - language_version: python3.8 - additional_dependencies: - - flake8-bandit - - flake8-blind-except - - flake8-docstrings - - flake8-logging-format - - flake8-print - exclude: ^tests|migrations - - # python static type checking - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 - hooks: - - id: mypy - additional_dependencies: - - types-geoip2 - args: - - --disallow-untyped-defs - - --disallow-incomplete-defs - - --check-untyped-defs - - --no-implicit-optional - - --ignore-missing-imports - - --follow-imports=silent - exclude: ^tests + # python static type checking + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.7.0 + hooks: + - id: mypy + args: + - --disallow-untyped-defs + - --disallow-incomplete-defs + - --check-untyped-defs + - --no-implicit-optional + - --ignore-missing-imports + - --follow-imports=silent diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..ccc1947 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,66 @@ +line-length = 88 +ignore = [ + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in __init__ + "D203", # 1 blank line required before class docstring + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D404", # First word of the docstring should not be "This" + "D405", # Section name should be properly capitalized + "D406", # Section name should end with a newline + "D407", # Missing dashed underline after section + "D410", # Missing blank line after section + "D411", # Missing blank line before section + "D412", # No blank lines allowed between a section header and its content + "D416", # Section name should end with a colon + "D417", + "D417", # Missing argument description in the docstring +] +select = [ + "A", # flake8 builtins + "C9", # mcabe + "D", # pydocstyle + "E", # pycodestyle (errors) + "F", # Pyflakes + "I", # isort + "S", # flake8-bandit + "T2", # flake8-print + "W", # pycodestype (warnings) +] + +[isort] +combine-as-imports = true + +[mccabe] +max-complexity = 8 + +[per-file-ignores] +"*tests/*" = [ + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D401", # First line should be in imperative mood + "D415", # First line should end with a period, question mark, or exclamation point + "E501", # Line too long + "E731", # Do not assign a lambda expression, use a def + "S101", # Use of assert detected + "S105", # Possible hardcoded password + "S106", # Possible hardcoded password + "S113", # Probable use of requests call with timeout set to {value} +] +"*/migrations/*" = [ + "E501", # Line too long +] +"*/settings.py" = [ + "F403", # from {name} import * used; unable to detect undefined names + "F405", # {name} may be undefined, or defined from star imports: +] +"*/settings/*" = [ + "F403", # from {name} import * used; unable to detect undefined names + "F405", # {name} may be undefined, or defined from star imports: +] diff --git a/README.md b/README.md index 43a52f2..c862b9b 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,24 @@ Opinionated Django app for managing "magic link" logins. **WARNING** -If you send a login link to the wrong person, they will gain full access to the user's account. Use -with extreme caution, and do not use this package without reading the source code and ensuring that -you are comfortable with it. If you have an internal security team, ask them to look at it before -using it. If your clients have security sign-off on your application, ask them to look at it before -using it. +If you send a login link to the wrong person, they will gain full access +to the user's account. Use with extreme caution, and do not use this +package without reading the source code and ensuring that you are +comfortable with it. If you have an internal security team, ask them to +look at it before using it. If your clients have security sign-off on +your application, ask them to look at it before using it. **/WARNING** -This app is not intended for general purpose URL tokenisation; it is designed to support a single -use case - so-called "magic link" logins. +This app is not intended for general purpose URL tokenisation; it is +designed to support a single use case - so-called "magic link" logins. -There are lots of alternative apps that can support this use case, including the project from which -this has been extracted - -[`django-request-token`](https://github.com/yunojuno/django-request-token). The reason for yet -another one is to handle the real-world challenge of URL caching / pre-fetch, where intermediaries -use URLs with unintended consequences. +There are lots of alternative apps that can support this use case, +including the project from which this has been extracted - +[`django-request-token`](https://github.com/yunojuno/django-request-token). +The reason for yet another one is to handle the real-world challenge of +URL caching / pre-fetch, where intermediaries use URLs with unintended +consequences. This packages supports a very specific model: @@ -29,18 +31,21 @@ This packages supports a very specific model: 4. User clicks on a button and performs a POST to the same page. 5. The POST request authenticates the user, and deactivates the token. -The advantage of this is the email clients do not support POST links, and any prefetch that attempts -a POST will fail the CSRF checks. +The advantage of this is the email clients do not support POST links, +and any prefetch that attempts a POST will fail the CSRF checks. -The purpose is to ensure that someone actively, purposefully, clicked on a link to authenticate -themselves. This enables instant deactivation of the token, so that it can no longer be used. +The purpose is to ensure that someone actively, purposefully, clicked on +a link to authenticate themselves. This enables instant deactivation of +the token, so that it can no longer be used. -In practice, without this check, valid magic links may be requested a number of times via GET -request before the intended recipient even sees the link. If you use a "max uses" restriction to -lock down the link you may find this limit is hit, and the end user then finds that the link is -inactive. The alternative to this is to remove the use limit and rely instead on an expiry window. -This risks leaving the token active even after the user has logged in. This package is targeted at -this situation. +In practice, without this check, valid magic links may be requested a +number of times via GET request before the intended recipient even sees +the link. If you use a "max uses" restriction to lock down the link you +may find this limit is hit, and the end user then finds that the link is +inactive. The alternative to this is to remove the use limit and rely +instead on an expiry window. This risks leaving the token active even +after the user has logged in. This package is targeted at this +situation. ## Use @@ -69,14 +74,17 @@ urlpatterns = [ ### Prerequisite: Override the default templates. -This package has two HTML templates that must be overridden in your local application. +This package has two HTML templates that must be overridden in your +local application. **templates/magic_link/logmein.html** -This is the landing page that a user sees when they click on the magic link. You can add any content -you like to this page - the only requirement is that must contains a simple form with a csrf token -and a submit button. This form must POST back to the link URL. The template render context includes -the `link` which has a `get_absolute_url` method to simplify this: +This is the landing page that a user sees when they click on the magic +link. You can add any content you like to this page - the only +requirement is that must contains a simple form with a csrf token and a +submit button. This form must POST back to the link URL. The template +render context includes the `link` which has a `get_absolute_url` method +to simplify this: ```html
@@ -87,8 +95,9 @@ the `link` which has a `get_absolute_url` method to simplify this: **templates/magic_link/error.html** -If the link has expired, been used, or is being accessed by someone who is already logged in, then -the `error.html` template will be rendered. The template context includes `link` and `error`. +If the link has expired, been used, or is being accessed by someone who +is already logged in, then the `error.html` template will be rendered. +The template context includes `link` and `error`. ```html

Error handling magic link {{ link }}: {{ error }}.

@@ -96,8 +105,8 @@ the `error.html` template will be rendered. The template context includes `link` ### 1. Create a new login link -The first step in managing magic links is to create one. Links are bound to a user, and can have a -custom post-login redirect URL. +The first step in managing magic links is to create one. Links are bound +to a user, and can have a custom post-login redirect URL. ```python # create a link with the default expiry and redirect @@ -112,16 +121,18 @@ url = request.build_absolute_uri(link.get_absolute_url()) ### 2. Send the link to the user -This package does not handle the sending on your behalf - it is your responsibility to ensure that -you send the link to the correct user. If you send the link to the wrong user, they will have full -access to the link user's account. **YOU HAVE BEEN WARNED**. +This package does not handle the sending on your behalf - it is your +responsibility to ensure that you send the link to the correct user. If +you send the link to the wrong user, they will have full access to the +link user's account. **YOU HAVE BEEN WARNED**. ## Auditing -A core requirement of this package is to be able to audit the use of links - for monitoring and -analysis. To enable this we have a second model, `MagicLinkUse`, and we create a new object for -every request to a link URL, _regardless of outcome_. Questions that we want to have answers for -include: +A core requirement of this package is to be able to audit the use of +links - for monitoring and analysis. To enable this we have a second +model, `MagicLinkUse`, and we create a new object for every request to a +link URL, _regardless of outcome_. Questions that we want to have +answers for include: - How long does it take for users to click on a link? - How many times is a link used before the POST login? @@ -130,47 +141,53 @@ include: - Can we identify common non-user client requests (email caches, bots, etc)? - Should we disable links after X non-POST requests? -In order to facilitate this analysis we denormalise a number of timestamps from the `MagicLinkUse` -object back onto the `MagicLink` itself: +In order to facilitate this analysis we denormalise a number of +timestamps from the `MagicLinkUse` object back onto the `MagicLink` +itself: - `created_at` - when the record was created in the database - `accessed_at` - the first GET request to the link URL - `logged_in_at` - the successful POST - `expires_at` - the link expiry, set when the link is created. -Note that the expiry timestamp is **not** updated when the link is used. This is by design, to -retain the original expiry timestamp. +Note that the expiry timestamp is **not** updated when the link is used. +This is by design, to retain the original expiry timestamp. ### Link validation -In addition to the timestamp fields, there is a separate boolean flag, `is_active`. This acts as a -"kill switch" that overrides any other attribute, and it allows a link to be disabled without having -to edit (or destroy) existing timestamp values. You can deactivate all links in one hit by calling -`MagicLink.objects.deactivate()`. +In addition to the timestamp fields, there is a separate boolean flag, +`is_active`. This acts as a "kill switch" that overrides any other +attribute, and it allows a link to be disabled without having to edit +(or destroy) existing timestamp values. You can deactivate all links in +one hit by calling `MagicLink.objects.deactivate()`. -A link's `is_valid` property combines both `is_active` and timestamp data to return a bool value -that defines whether a link can used, based on the following criteria: +A link's `is_valid` property combines both `is_active` and timestamp +data to return a bool value that defines whether a link can used, based +on the following criteria: 1. The link is active (`is_active`) 2. The link has not expired (`expires_at`) 3. The link has not already been used (`logged_in_at`) -In addition to checking the property `is_valid`, the `validate()` method will raise an exception -based on the specific condition that failed. This is used by the link view to give feedback to the -user on the nature of the failure. +In addition to checking the property `is_valid`, the `validate()` method +will raise an exception based on the specific condition that failed. +This is used by the link view to give feedback to the user on the nature +of the failure. ### Request authorization -If the link's `is_valid` property returns `True`, then the link _can_ be used. However, this does -not mean that the link can be used by anyone. We do not allow authenticated users to login using -someone else's magic link. The `authorize()` method takes a `User` argument and determines whether -they are authorized to use the link. If the user is authenticated, and does not match the -`link.user`, then a `PermissionDenied` exception is raised. +If the link's `is_valid` property returns `True`, then the link _can_ be +used. However, this does not mean that the link can be used by anyone. +We do not allow authenticated users to login using someone else's magic +link. The `authorize()` method takes a `User` argument and determines +whether they are authorized to use the link. If the user is +authenticated, and does not match the `link.user`, then a +`PermissionDenied` exception is raised. ### Putting it together -Combining the validation, authorization and auditing, we get a simplified flow that looks something -like this: +Combining the validation, authorization and auditing, we get a +simplified flow that looks something like this: ```python def get(request, token): diff --git a/magic_link/__init__.py b/magic_link/__init__.py index 9a3ad83..e69de29 100644 --- a/magic_link/__init__.py +++ b/magic_link/__init__.py @@ -1 +0,0 @@ -default_app_config = "magic_link.apps.MagicLinkAppConfig" diff --git a/magic_link/admin.py b/magic_link/admin.py index b098215..783c89f 100644 --- a/magic_link/admin.py +++ b/magic_link/admin.py @@ -30,7 +30,6 @@ class MagicLinkUseInline(LoggedInMixin, admin.TabularInline): class MagicLinkAdmin(admin.ModelAdmin): - list_display = ( "token", "user", @@ -70,7 +69,6 @@ def valid(self, obj: MagicLink) -> bool: class MagicLinkUseAdmin(LoggedInMixin, admin.ModelAdmin): - list_display = ("link", "http_method", "session_key", "logged_in") search_fields = ( "session_key", diff --git a/magic_link/apps.py b/magic_link/apps.py index a01e6d0..df3e64a 100644 --- a/magic_link/apps.py +++ b/magic_link/apps.py @@ -2,6 +2,6 @@ class MagicLinkAppConfig(AppConfig): - name = "magic_link" verbose_name = "Magic links" + default_auto_field = "django.db.models.AutoField" diff --git a/magic_link/migrations/0001_initial.py b/magic_link/migrations/0001_initial.py index 1169f3a..4c53437 100644 --- a/magic_link/migrations/0001_initial.py +++ b/magic_link/migrations/0001_initial.py @@ -10,7 +10,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/magic_link/migrations/0002_magiclink_logged_in_at.py b/magic_link/migrations/0002_magiclink_logged_in_at.py index ec0efd9..1e3b323 100644 --- a/magic_link/migrations/0002_magiclink_logged_in_at.py +++ b/magic_link/migrations/0002_magiclink_logged_in_at.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("magic_link", "0001_initial"), ] diff --git a/magic_link/migrations/0003_magiclink_accessed_at.py b/magic_link/migrations/0003_magiclink_accessed_at.py index ffeb73b..41fc5cb 100644 --- a/magic_link/migrations/0003_magiclink_accessed_at.py +++ b/magic_link/migrations/0003_magiclink_accessed_at.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("magic_link", "0002_magiclink_logged_in_at"), ] diff --git a/magic_link/migrations/0004_remove_magiclinkuse_link_is_valid.py b/magic_link/migrations/0004_remove_magiclinkuse_link_is_valid.py index 079ced2..54fe5d8 100644 --- a/magic_link/migrations/0004_remove_magiclinkuse_link_is_valid.py +++ b/magic_link/migrations/0004_remove_magiclinkuse_link_is_valid.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("magic_link", "0003_magiclink_accessed_at"), ] diff --git a/pyproject.toml b/pyproject.toml index 64e5005..18e7597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-magic-link" -version = "0.5.1" +version = "1.0.0" description = "Django app for managing tokenised 'magic link' logins." license = "MIT" authors = ["YunoJuno "] @@ -10,42 +10,38 @@ homepage = "https://github.com/yunojuno/django-magic-link" repository = "https://github.com/yunojuno/django-magic-link" documentation = "https://github.com/yunojuno/django-magic-link" classifiers = [ - "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.0", - "Framework :: Django :: 3.1", "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] packages = [{ include = "magic_link" }] [tool.poetry.dependencies] -python = "^3.7" -django = "^2.2 || ^3.0 || ^4.0" +python = "^3.8" +django = "^3.2 || ^4.0 || ^5.0" [tool.poetry.dev-dependencies] -black = {version = "*", allows-prereleases = true} +black = "*" coverage = "*" -flake8-bandit = "*" -flake8-blind-except = "*" -flake8-docstrings = "*" -flake8-logging-format = "*" -flake8-print = "*" freezegun = "*" -isort = "*" mypy = "*" pre-commit = "*" pytest = "*" pytest-cov = "*" pytest-django = "*" -pyupgrade = "*" +ruff = "*" tox = "*" [build-system] diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py index 46dbcb9..85207f0 100644 --- a/tests/migrations/0001_initial.py +++ b/tests/migrations/0001_initial.py @@ -7,7 +7,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/tests/settings.py b/tests/settings.py index 0b70335..eeaf66c 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -43,6 +43,7 @@ "context_processors": [ "django.contrib.messages.context_processors.messages", "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", ] }, } diff --git a/tox.ini b/tox.ini index 2f969c4..b7fe7bd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,45 +1,54 @@ [tox] isolated_build = True -envlist = fmt, lint, mypy, py{3.7,3.8,3.9}-django{22,30,31,32,main} +envlist = + fmt, lint, mypy, + django-checks, + ; https://docs.djangoproject.com/en/5.0/releases/ + django32-py{38,39,310} + django40-py{38,39,310} + django41-py{38,39,310,311} + django42-py{38,39,310,311} + django50-py{310,311,312} + djangomain-py{311,312} [testenv] deps = coverage - freezegun pytest pytest-cov pytest-django - django22: Django>=2.2,<2.3 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 django32: Django>=3.2,<3.3 + django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 + django50: https://github.com/django/django/archive/stable/5.0.x.tar.gz djangomain: https://github.com/django/django/archive/main.tar.gz commands = - pytest --cov=magic_link tests/ + pytest --cov=magic_link --verbose tests/ + +[testenv:django-checks] +description = Django system checks and missing migrations +deps = Django +commands = + python manage.py check --fail-level WARNING + python manage.py makemigrations --dry-run --check --verbosity 3 [testenv:fmt] -description = Python source code formatting (isort, black) +description = Python source code formatting (black) deps = - isort black commands = - isort --check-only magic_link black --check magic_link [testenv:lint] -description = Python source code linting (pylint, flake8, bandit, pydocstyle) +description = Python source code linting (ruff) deps = - flake8 - flake8-bandit - flake8-blind-except - flake8-docstrings - flake8-logging-format - flake8-print + ruff commands = - flake8 magic_link + ruff magic_link [testenv:mypy] description = Python source code type hints (mypy)