File django-test-migrations-1.5.0.obscpio of Package python-django-test-migrations
07070100000000000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002200000000django-test-migrations-1.5.0/.dev07070100000001000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002A00000000django-test-migrations-1.5.0/.dev/scripts07070100000002000081A40000000000000000000000016802260C0000017A000000000000000000000000000000000000004E00000000django-test-migrations-1.5.0/.dev/scripts/ci-mysql-setup-integration-tests.sh#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
set -o xtrace
_SETUP_SCRIPT="$(cat << EOM
CREATE DATABASE IF NOT EXISTS db;
GRANT ALL PRIVILEGES ON db.* TO django;
FLUSH PRIVILEGES;
EOM
)"
mysql \
--host="${DJANGO_DATABASE_HOST}" \
--port="${DJANGO_DATABASE_PORT}" \
--user="root" \
--password="superpasswd123" \
--execute="${_SETUP_SCRIPT}"
07070100000003000081A40000000000000000000000016802260C00000149000000000000000000000000000000000000002B00000000django-test-migrations-1.5.0/.editorconfig# Check http://editorconfig.org for more information
# This is the main config file for this project:
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
indent_style = space
insert_final_newline = true
indent_size = 2
[*.py]
indent_size = 4
[*.pyi]
indent_size = 4
[Makefile]
indent_style = tab
07070100000004000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002500000000django-test-migrations-1.5.0/.github07070100000005000081A40000000000000000000000016802260C00000119000000000000000000000000000000000000003400000000django-test-migrations-1.5.0/.github/dependabot.yml---
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
time: "02:00"
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
time: "02:00"
open-pull-requests-limit: 10
07070100000006000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002F00000000django-test-migrations-1.5.0/.github/workflows07070100000007000081A40000000000000000000000016802260C000012FF000000000000000000000000000000000000003800000000django-test-migrations-1.5.0/.github/workflows/test.yml---
name: test
'on':
push:
branches:
- master
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
env:
DJANGO_DATABASE_ENGINE: "${{ matrix.env.DJANGO_DATABASE_ENGINE || 'django.db.backends.sqlite3' }}"
DJANGO_DATABASE_USER: django
DJANGO_DATABASE_PASSWORD: passwd123
DJANGO_DATABASE_NAME: db
DJANGO_DATABASE_HOST: 127.0.0.1
DJANGO_DATABASE_PORT: "${{ matrix.env.DJANGO_DATABASE_PORT }}"
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
django-version:
- 'Django~=4.2.0'
- 'Django~=5.1.0'
- 'Django~=5.2.0'
docker-compose-services: ['']
additional-dependencies: ['']
env: [{}]
integration-test-setup-script: ['']
include:
- python-version: '3.12'
django-version: 'Django~=5.1.0'
docker-compose-services: postgresql-db
additional-dependencies: psycopg2
env:
DJANGO_DATABASE_ENGINE: 'django.db.backends.postgresql'
DJANGO_DATABASE_PORT: 5432
- python-version: '3.13'
django-version: 'Django~=5.2.0'
docker-compose-services: postgresql-db-17
additional-dependencies: psycopg
env:
DJANGO_DATABASE_ENGINE: 'django.db.backends.postgresql'
DJANGO_DATABASE_PORT: 5433
# TODO: reenable
# - python-version: '3.12'
# django-version: 'Django~=5.0.0'
# docker-compose-services: mysql-db
# additional-dependencies: mysqlclient
# env:
# DJANGO_DATABASE_ENGINE: 'django.db.backends.mysql'
# DJANGO_DATABASE_PORT: 3306
# integration-test-setup-script: >-
# ./.dev/scripts/ci-mysql-setup-integration-tests.sh
- python-version: '3.12'
django-version: 'Django~=5.2.0'
docker-compose-services: maria-db
additional-dependencies: mysqlclient
env:
DJANGO_DATABASE_ENGINE: 'django.db.backends.mysql'
DJANGO_DATABASE_PORT: 3307
integration-test-setup-script: >-
./.dev/scripts/ci-mysql-setup-integration-tests.sh
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: |
curl -sSL "https://install.python-poetry.org" | python
# Adding `poetry` to `$PATH`:
echo "$HOME/.poetry/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
poetry config virtualenvs.in-project true
poetry run pip install -U pip
poetry install
poetry run pip install \
--upgrade \
"${{ matrix.django-version }}" \
${{ matrix.additional-dependencies }}
- name: Pull and build docker compose services
if: ${{ matrix.docker-compose-services }}
run: |
docker compose pull ${{ matrix.docker-compose-services }}
docker compose up --detach ${{ matrix.docker-compose-services }}
- name: Wait for docker-compose services
if: ${{ matrix.docker-compose-services }}
run: |
sudo apt-get update && sudo apt-get install -y wait-for-it
wait-for-it \
--host='localhost' \
--port="${{ matrix.env.DJANGO_DATABASE_PORT }}" \
--timeout=30 \
--strict
- name: "Run checks for python ${{ matrix.python-version }} and django ${{ matrix.django-version }}"
run: make test
- name: >-
Run integration tests for python ${{ matrix.python-version }}
and django ${{ matrix.django-version }} using
${{ matrix.docker-compose-services }}
if: ${{ matrix.docker-compose-services }}
run: |
if [ -f '${{ matrix.integration-test-setup-script }}' ]; then
bash '${{ matrix.integration-test-setup-script }}'
fi
CHECK_OUTPUT="$(poetry run python django_test_app/manage.py check 2>&1 || true)"
echo "${CHECK_OUTPUT}"
echo "${CHECK_OUTPUT}" \
| grep --quiet --extended-regexp '^System check identified 4 issues'
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files:
./coverage.xml
- name: Stop docker-compose services
if: ${{ always() && matrix.docker-compose-services }}
run: docker compose down || true
07070100000008000081A40000000000000000000000016802260C00000B5D000000000000000000000000000000000000002800000000django-test-migrations-1.5.0/.gitignore#### joe made this: http://goel.io/joe
#### python ####
# Byte-compiled / optimized / DLL files
.pytest_cache
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
pip-wheel-metadata/
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
#### macos ####
# General
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
#### windows ####
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
#### linux ####
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
#### jetbrains ####
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Custom ###
ex.py
*.sqlite3
07070100000009000081A40000000000000000000000016802260C000009DB000000000000000000000000000000000000002A00000000django-test-migrations-1.5.0/CHANGELOG.md# Version history
We follow Semantic Versions since the `0.1.0` release.
## Version 1.5.0
### Features
- Adds Python 3.13 support
- Drops Python 3.9 support
- Adds Django 5.2 support
## Version 1.4.0
### Features
- Adds Python 3.12 support
- Drops Python 3.8 support
- Updates `typing_extensions` to `>=4,<5`
- Adds more typing to the project
### Fixes
- Fixes getting the `statement_timeout` setting name on MariaDB servers
- Fixes delayed apps cache
## Version 1.3.0
### Features
- Adds Python 3.11 support
- Drops Python 3.7 support
- Adds Django 4.1 support
- Adds Django 4.2 support
- Drops Django 2.2 support
## Version 1.2.0
### Features
- Adds Python 3.10
- Adds Django 4.0 support
- Updates `typing_extensions` to `>=3.6,<5`
## Version 1.1.0
### Features
- Adds Django 3.1 support (#123, #154)
- Adds markers/tags to migration tests (#138)
- Adds database configuration checks (#91)
### Bugfixes
- Fixes tables dropping on MySQL by disabling foreign keys checks (#149)
- Fixes migrate signals muting when running migrations tests (#133)
### Misc
- Runs tests against PostgreSQL and MySQL database engines (#129)
## Version 1.0.0
### Breaking Changes
- Rename following `Migrator` methods (#83):
+ `before` to `apply_initial_migration`
+ `after` to `apply_tested_migration`
- Improves databases setup and teardown for migrations tests (#76)
Currently `Migrator.reset` uses `migrate` management command and all logic
related to migrations tests setup is moved to
`Migrator.apply_tested_migration`.
### Bugfixes
- Fixes `pre_migrate` and `post_migrate` signals muting (#87)
- Adds missing `typing_extension` dependency (#86)
### Misc
- Refactor tests (#79)
- Return `django` installed from `master` branch to testing matrix (#77)
## Version 0.3.0
### Features
- Drops `django@2.1` support
- Adds `'*'` alias for ignoring
all migrations in an app with `DTM_IGNORED_MIGRATIONS`
### Bugfixes
- Fixes how `pre_migrate` and `post_migrate` signals are muted
### Misc
- Updates `wemake-python-styleguide`
- Moves from `travis` to Github Actions
## Version 0.2.0
### Features
- Adds `autoname` check to forbid `*_auto_*` named migrations
- Adds `django@3.0` support
- Adds `python3.8` support
### Bugfixes
- Fixes that migtaions were failing with `pre_migrate` and `post_migrate` signals
- Fixes that tests were failing when `pytest --nomigration` was executed,
now they are skipped
### Misc
- Updates to `poetry@1.0`
## Version 0.1.0
- Initial release
0707010000000A000081A40000000000000000000000016802260C00000430000000000000000000000000000000000000002500000000django-test-migrations-1.5.0/LICENSEMIT License
Copyright (c) 2019 wemake.services
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
0707010000000B000081A40000000000000000000000016802260C00000218000000000000000000000000000000000000002600000000django-test-migrations-1.5.0/MakefileSHELL:=/usr/bin/env bash
.PHONY: format
format:
poetry run ruff format
poetry run ruff check
.PHONY: lint
lint:
poetry run ruff check --exit-non-zero-on-fix --diff
poetry run ruff format --check --diff
poetry run mypy django_test_migrations
poetry run flake8 .
.PHONY: unit
unit:
# We need one more test run to make sure that `--nomigrations` work:
poetry run pytest -p no:cov -o addopts="" --nomigrations
poetry run pytest
.PHONY: package
package:
poetry check
poetry run pip check
.PHONY: test
test: lint unit package
0707010000000C000081A40000000000000000000000016802260C00002FDA000000000000000000000000000000000000002700000000django-test-migrations-1.5.0/README.md# django-test-migrations
[](https://wemake-services.github.io)
[](https://github.com/wemake-services/django-test-migrations/actions?query=workflow%3Atest)
[](https://codecov.io/gh/wemake-services/django-test-migrations)
[](https://pypi.org/project/django-test-migrations/)

[](https://github.com/wemake-services/wemake-python-styleguide)
## Features
- Allows to test `django` schema and data migrations
- Allows to test both forward and rollback migrations
- Allows to test the migrations order
- Allows to test migration names
- Allows to test database configuration
- Fully typed with annotations and checked with `mypy`, [PEP561 compatible](https://www.python.org/dev/peps/pep-0561/)
- Easy to start: has lots of docs, tests, and tutorials
Read the [announcing post](https://sobolevn.me/2019/10/testing-django-migrations).
See real-world [usage example](https://github.com/wemake-services/wemake-django-template).
## Installation
```bash
pip install django-test-migrations
```
We support several `django` versions:
- `3.2`
- `4.1`
- `4.2`
- `5.0`
Other versions most likely will work too,
but they are not officially supported.
## Testing Django migrations
Testing migrations is not a frequent thing in `django` land.
But, sometimes it is totally required. When?
When we do complex schema or data changes
and what to be sure that existing data won't be corrupted.
We might also want to be sure that all migrations can be safely rolled back.
And as a final touch, we want to be sure that migrations
are in the correct order and have correct dependencies.
### Testing forward migrations
To test all migrations we have a [`Migrator`](https://github.com/wemake-services/django-test-migrations/blob/master/django_test_migrations/migrator.py) class.
It has three methods to work with:
- `.apply_initial_migration()` which takes app and migration names to generate
a state before the actual migration happens. It creates the `before state`
by applying all migrations up to and including the ones passed as an argument.
- `.apply_tested_migration()` which takes app and migration names to perform the
actual migration
- `.reset()` to clean everything up after we are done with testing
So, here's an example:
```python
from django_test_migrations.migrator import Migrator
migrator = Migrator(database='default')
# Initial migration, currently our model has only a single string field:
# Note:
# We are testing migration `0002_someitem_is_clean`, so we are specifying
# the name of the previous migration (`0001_initial`) in the
# .apply_initial_migration() method in order to prepare a state of the database
# before applying the migration we are going to test.
#
old_state = migrator.apply_initial_migration(('main_app', '0001_initial'))
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
# Let's create a model with just a single field specified:
SomeItem.objects.create(string_field='a')
assert len(SomeItem._meta.get_fields()) == 2 # id + string_field
# Now this migration will add `is_clean` field to the model:
new_state = migrator.apply_tested_migration(
('main_app', '0002_someitem_is_clean'),
)
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
# We can now test how our migration worked, new field is there:
assert SomeItem.objects.filter(is_clean=True).count() == 0
assert len(SomeItem._meta.get_fields()) == 3 # id + string_field + is_clean
# Cleanup:
migrator.reset()
```
That was an example of a forward migration.
### Backward migration
The thing is that you can also test backward migrations.
Nothing really changes except migration names that you pass and your logic:
```python
migrator = Migrator()
# Currently our model has two field, but we need a rollback:
old_state = migrator.apply_initial_migration(
('main_app', '0002_someitem_is_clean'),
)
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
# Create some data to illustrate your cases:
# ...
# Now this migration will drop `is_clean` field:
new_state = migrator.apply_tested_migration(('main_app', '0001_initial'))
# Assert the results:
# ...
# Cleanup:
migrator.reset()
```
### Testing migrations ordering
Sometimes we also want to be sure that our migrations are in the correct order
and that all our `dependencies = [...]` are correct.
To achieve that we have [`plan.py`](https://github.com/wemake-services/django-test-migrations/blob/master/django_test_migrations/plan.py) module.
That's how it can be used:
```python
from django_test_migrations.plan import all_migrations, nodes_to_tuples
main_migrations = all_migrations('default', ['main_app', 'other_app'])
assert nodes_to_tuples(main_migrations) == [
('main_app', '0001_initial'),
('main_app', '0002_someitem_is_clean'),
('other_app', '0001_initial'),
('main_app', '0003_update_is_clean'),
('main_app', '0004_auto_20191119_2125'),
('other_app', '0002_auto_20191120_2230'),
]
```
This way you can be sure that migrations
and apps that depend on each other will be executed in the correct order.
### `factory_boy` integration
If you use factories to create models, you can replace their respective
`.build()` or `.create()` calls with methods of `factory` and pass the
model name and factory class as arguments:
```python
import factory
old_state = migrator.apply_initial_migration(
('main_app', '0002_someitem_is_clean'),
)
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
# instead of
# item = SomeItemFactory.create()
# use this:
factory.create(SomeItem, FACTORY_CLASS=SomeItemFactory)
# ...
```
## Test framework integrations 🐍
We support several test frameworks as first-class citizens.
That's a testing tool after all!
Note that the Django `post_migrate` signal's receiver list is cleared at
the start of tests and restored afterwards. If you need to test your
own `post_migrate` signals then attach/remove them during a test.
### pytest
We ship `django-test-migrations` with a `pytest` plugin
that provides two convenient fixtures:
- `migrator_factory` that gives you an opportunity
to create `Migrator` classes for any database
- `migrator` instance for the `'default'` database
That's how it can be used:
```python
import pytest
@pytest.mark.django_db
def test_pytest_plugin_initial(migrator):
"""Ensures that the initial migration works."""
old_state = migrator.apply_initial_migration(('main_app', None))
with pytest.raises(LookupError):
# Model does not yet exist:
old_state.apps.get_model('main_app', 'SomeItem')
new_state = migrator.apply_tested_migration(('main_app', '0001_initial'))
# After the initial migration is done, we can use the model state:
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.filter(string_field='').count() == 0
```
### unittest
We also ship an integration with the built-in `unittest` framework.
Here's how it can be used:
```python
from django_test_migrations.contrib.unittest_case import MigratorTestCase
class TestDirectMigration(MigratorTestCase):
"""This class is used to test direct migrations."""
migrate_from = ('main_app', '0002_someitem_is_clean')
migrate_to = ('main_app', '0003_update_is_clean')
def prepare(self):
"""Prepare some data before the migration."""
SomeItem = self.old_state.apps.get_model('main_app', 'SomeItem')
SomeItem.objects.create(string_field='a')
SomeItem.objects.create(string_field='a b')
def test_migration_main0003(self):
"""Run the test itself."""
SomeItem = self.new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 1
```
### Choosing only migrations tests
In CI systems it is important to get instant feedback. Running tests that
apply database migration can slow down tests execution, so it is often a good
idea to run standard, fast, regular unit tests without migrations in parallel
with slower migrations tests.
#### pytest
`django_test_migrations` adds `migration_test` marker to each test using
`migrator_factory` or `migrator` fixture.
To run only migrations test, use `-m` option:
```bash
pytest -m migration_test # Runs only migration tests
pytest -m "not migration_test" # Runs all except migration tests
```
#### unittest
`django_test_migrations` adds `migration_test`
[tag](https://docs.djangoproject.com/en/3.0/topics/testing/tools/#tagging-tests)
to every `MigratorTestCase` subclass.
To run only migrations tests, use `--tag` option:
```bash
python mange.py test --tag=migration_test # Runs only migration tests
python mange.py test --exclude-tag=migration_test # Runs all except migration tests
```
## Django Checks
`django_test_migrations` comes with 2 groups of Django's checks for:
+ detecting migrations scripts automatically generated names
+ validating some subset of database settings
### Testing migration names
`django` generates migration names for you when you run `makemigrations`.
These names are bad ([read more](https://adamj.eu/tech/2020/02/24/how-to-disallow-auto-named-django-migrations/) about why it is bad)!
Just look at this: `0004_auto_20191119_2125.py`
What does this migration do? What changes does it have?
One can also pass `--name` attribute when creating migrations, but it is easy to forget.
We offer an automated solution: `django` check
that produces an error for each badly named migration.
Add our check into your `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
# ...
# Our custom check:
'django_test_migrations.contrib.django_checks.AutoNames',
]
```
Then in your CI run:
```bash
python manage.py check --deploy
```
This way you will be safe from wrong names in your migrations.
Do you have a migrations that cannot be renamed? Add them to the ignore list:
```python
# settings.py
DTM_IGNORED_MIGRATIONS = {
('main_app', '0004_auto_20191119_2125'),
('dependency_app', '0001_auto_20201110_2100'),
}
```
Then we won't complain about them.
Or you can completely ignore entire app:
```python
# settings.py
DTM_IGNORED_MIGRATIONS = {
('dependency_app', '*'),
('another_dependency_app', '*'),
}
```
### Database configuration
Add our check to `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
# ...
# Our custom check:
'django_test_migrations.contrib.django_checks.DatabaseConfiguration',
]
```
Then just run `check` management command in your CI like listed in section
above.
## Related projects
You might also like:
- [django-migration-linter](https://github.com/3YOURMIND/django-migration-linter) - Detect backward incompatible migrations for your django project.
- [wemake-django-template](https://github.com/wemake-services/wemake-django-template/) - Bleeding edge django template focused on code quality and security with both `django-test-migrations` and `django-migration-linter` on board.
## Credits
This project is based on work of other awesome people:
- [@asfaltboy](https://gist.github.com/asfaltboy/b3e6f9b5d95af8ba2cc46f2ba6eae5e2)
- [@blueyed](https://gist.github.com/blueyed/4fb0a807104551f103e6)
- [@fernandogrd](https://gist.github.com/blueyed/4fb0a807104551f103e6#gistcomment-1546191)
- [@adamchainz](https://adamj.eu/tech/2020/02/24/how-to-disallow-auto-named-django-migrations/)
## License
MIT.
0707010000000D000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002D00000000django-test-migrations-1.5.0/django_test_app0707010000000E000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003D00000000django-test-migrations-1.5.0/django_test_app/django_test_app0707010000000F000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004900000000django-test-migrations-1.5.0/django_test_app/django_test_app/__init__.py07070100000010000081A40000000000000000000000016802260C00000CD8000000000000000000000000000000000000004900000000django-test-migrations-1.5.0/django_test_app/django_test_app/settings.py"""Django settings for django_test_app project."""
import os
from pathlib import Path
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = Path(__file__).parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_dpvr*#hjgv)6v=potf%*+$na7_ck(*+^g08lw0^44zoo88)wb' # noqa: S105
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Our custom checks:
'django_test_migrations.contrib.django_checks.AutoNames',
'django_test_migrations.contrib.django_checks.DatabaseConfiguration',
# Custom:
'main_app',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'django_test_app.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'django_test_app.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
_DATABASE_NAME = os.environ.get(
'DJANGO_DATABASE_NAME',
default=BASE_DIR.joinpath('db.sqlite3'),
)
DATABASES = {
'default': {
'ENGINE': os.environ.get(
'DJANGO_DATABASE_ENGINE',
default='django.db.backends.sqlite3',
),
'USER': os.environ.get('DJANGO_DATABASE_USER', default=''),
'PASSWORD': os.environ.get('DJANGO_DATABASE_PASSWORD', default=''),
'NAME': _DATABASE_NAME,
'PORT': os.environ.get('DJANGO_DATABASE_PORT', default=''),
'HOST': os.environ.get('DJANGO_DATABASE_HOST', default=''),
'TEST': {
'NAME': (
_DATABASE_NAME
if _DATABASE_NAME.startswith('test_')
else f'test_{_DATABASE_NAME}'
),
},
},
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = []
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'
07070100000011000081A40000000000000000000000016802260C0000028D000000000000000000000000000000000000004500000000django-test-migrations-1.5.0/django_test_app/django_test_app/urls.py"""
django_test_app URL Configuration.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
urlpatterns = []
07070100000012000081A40000000000000000000000016802260C00000197000000000000000000000000000000000000004500000000django-test-migrations-1.5.0/django_test_app/django_test_app/wsgi.py"""
WSGI config for django_test_app project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test_app.settings')
application = get_wsgi_application()
07070100000013000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003600000000django-test-migrations-1.5.0/django_test_app/main_app07070100000014000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004200000000django-test-migrations-1.5.0/django_test_app/main_app/__init__.py07070100000015000081A40000000000000000000000016802260C00000085000000000000000000000000000000000000003E00000000django-test-migrations-1.5.0/django_test_app/main_app/apps.pyfrom django.apps import AppConfig
class MainAppConfig(AppConfig):
"""Configuration for ``main_app``."""
name = 'main_app'
07070100000016000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003C00000000django-test-migrations-1.5.0/django_test_app/main_app/logic07070100000017000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004800000000django-test-migrations-1.5.0/django_test_app/main_app/logic/__init__.py07070100000018000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004100000000django-test-migrations-1.5.0/django_test_app/main_app/logic/pure07070100000019000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004D00000000django-test-migrations-1.5.0/django_test_app/main_app/logic/pure/__init__.py0707010000001A000081A40000000000000000000000016802260C00000100000000000000000000000000000000000000004F00000000django-test-migrations-1.5.0/django_test_app/main_app/logic/pure/migrations.pyimport typing
if typing.TYPE_CHECKING:
from main_app.models import SomeItem
def is_clean_item(instance: 'SomeItem') -> bool:
"""Pure function that decides whether or not whitespace is in the model."""
return ' ' not in instance.string_field
0707010000001B000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004100000000django-test-migrations-1.5.0/django_test_app/main_app/migrations0707010000001C000081A40000000000000000000000016802260C000002F3000000000000000000000000000000000000005100000000django-test-migrations-1.5.0/django_test_app/main_app/migrations/0001_initial.py# Generated by Django 2.2.7 on 2019-11-19 20:00
from django.db import migrations, models
class Migration(migrations.Migration):
"""Initial migration."""
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='SomeItem',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'string_field',
models.CharField(max_length=50),
),
],
),
]
0707010000001D000081A40000000000000000000000016802260C000001BB000000000000000000000000000000000000005B00000000django-test-migrations-1.5.0/django_test_app/main_app/migrations/0002_someitem_is_clean.py# Generated by Django 2.2.7 on 2019-11-19 21:24
from django.db import migrations, models
class Migration(migrations.Migration):
"""Migration to add ``is_clean`` field to ``SomeItem``."""
dependencies = [
('main_app', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='someitem',
name='is_clean',
field=models.BooleanField(default=True),
),
]
0707010000001E000081A40000000000000000000000016802260C000004B4000000000000000000000000000000000000005900000000django-test-migrations-1.5.0/django_test_app/main_app/migrations/0003_update_is_clean.py# Generated by Django 2.2.7 on 2019-11-19 21:25
from django.db import migrations
from main_app.logic.pure.migrations import is_clean_item
def _set_clean_flag(apps, schema_editor):
"""
Performs the data migration.
We can't import the ``SomeItem`` model directly as it may be a newer
version than this migration expects. We use the historical version.
We are using ``.all()`` because
we don't have a lot of ``SomeItem`` instances.
In real-life you should not do that.
"""
SomeItem = apps.get_model('main_app', 'SomeItem')
for instance in SomeItem.objects.all():
instance.is_clean = is_clean_item(instance)
instance.save(update_fields=['is_clean'])
def _remove_clean_flags(apps, schema_editor):
"""
This is just a noop example of a rollback function.
It is not used in our simple case,
but it should be implemented for more complex scenarios.
"""
class Migration(migrations.Migration):
"""Performs the logical data migration for ``SomeItem``."""
dependencies = [
('main_app', '0002_someitem_is_clean'),
]
operations = [
migrations.RunPython(_set_clean_flag, _remove_clean_flags),
]
0707010000001F000081A40000000000000000000000016802260C00000235000000000000000000000000000000000000005C00000000django-test-migrations-1.5.0/django_test_app/main_app/migrations/0004_auto_20191119_2125.py# Generated by Django 2.2.7 on 2019-11-19 21:25
"""
This migration is named incorrectly.
We use it as a test for wrong autonames.
Please, do not rename it!
"""
from django.db import migrations, models
class Migration(migrations.Migration):
"""Removes the default value from ``is_clean`` from ``SomeItem``."""
dependencies = [
('main_app', '0003_update_is_clean'),
]
operations = [
migrations.AlterField(
model_name='someitem',
name='is_clean',
field=models.BooleanField(),
),
]
07070100000020000081A40000000000000000000000016802260C0000016B000000000000000000000000000000000000005C00000000django-test-migrations-1.5.0/django_test_app/main_app/migrations/0005_auto_20200329_1118.py# Generated by Django 2.2.10 on 2020-03-29 11:18
"""
This migration is named incorrectly.
We use it as a test for wrong autonames.
Please, do not rename it!
"""
from django.db import migrations
class Migration(migrations.Migration):
"""Dummy migration."""
dependencies = [
('main_app', '0004_auto_20191119_2125'),
]
operations = []
07070100000021000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004D00000000django-test-migrations-1.5.0/django_test_app/main_app/migrations/__init__.py07070100000022000081A40000000000000000000000016802260C00000103000000000000000000000000000000000000004000000000django-test-migrations-1.5.0/django_test_app/main_app/models.pyfrom django.db import models
_SomeItemStringFieldLength = 50
class SomeItem(models.Model):
"""We use this model for testing migrations."""
string_field = models.CharField(max_length=_SomeItemStringFieldLength)
is_clean = models.BooleanField()
07070100000023000081A40000000000000000000000016802260C00000032000000000000000000000000000000000000003F00000000django-test-migrations-1.5.0/django_test_app/main_app/views.pyfrom django.shortcuts import render # noqa: F401
07070100000024000081ED0000000000000000000000016802260C0000013C000000000000000000000000000000000000003700000000django-test-migrations-1.5.0/django_test_app/manage.py#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
from django.core.management import execute_from_command_line
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test_app.settings')
execute_from_command_line(sys.argv)
07070100000025000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003400000000django-test-migrations-1.5.0/django_test_migrations07070100000026000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004000000000django-test-migrations-1.5.0/django_test_migrations/__init__.py07070100000027000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003B00000000django-test-migrations-1.5.0/django_test_migrations/checks07070100000028000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004700000000django-test-migrations-1.5.0/django_test_migrations/checks/__init__.py07070100000029000081A40000000000000000000000016802260C00000ADE000000000000000000000000000000000000004800000000django-test-migrations-1.5.0/django_test_migrations/checks/autonames.pyfrom collections.abc import Sequence
from fnmatch import fnmatch
from typing import Final
from django.conf import settings
from django.core.checks import CheckMessage
from django.core.checks import Warning as DjangoWarning
_IgnoreAppSpec = frozenset[str]
_IgnoreMigrationSpec = frozenset[tuple[str, str]]
#: We use this type hint to represent ignore rules for migrations.
_IgnoreSpec = tuple[_IgnoreAppSpec, _IgnoreMigrationSpec]
#: We use this value as a unique identifier of this check.
CHECK_NAME: Final = 'django_test_migrations.checks.autonames'
#: Settings name for this check to ignore some migrations.
_SETTINGS_NAME: Final = 'DTM_IGNORED_MIGRATIONS'
# Special key to ignore all migrations inside an app
_IGNORE_APP_MIGRATIONS_SPECIAL_KEY: Final = '*'
def _is_ignored(
app_label: str,
migration_name: str,
ignored: _IgnoreSpec,
) -> bool:
ignored_apps, ignored_migrations = ignored
return (
app_label in ignored_apps
or (app_label, migration_name) in ignored_migrations
)
def _build_ignores() -> _IgnoreSpec:
ignored_migrations: _IgnoreMigrationSpec = getattr(
settings,
_SETTINGS_NAME,
frozenset(),
)
ignored_apps: _IgnoreAppSpec = frozenset(
app_label
for app_label, migration_name in ignored_migrations
if migration_name == _IGNORE_APP_MIGRATIONS_SPECIAL_KEY
)
return ignored_apps, ignored_migrations
def check_migration_names(
*args: object,
**kwargs: object,
) -> Sequence[CheckMessage]:
"""
Finds automatic names in available migrations.
We use nested import here, because some versions of django fails otherwise.
They do raise:
``django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.``
"""
from django.db.migrations.loader import MigrationLoader # noqa: PLC0415
loader = MigrationLoader(None, ignore_no_migrations=True)
loader.load_disk()
messages = []
ignores = _build_ignores()
for app_label, migration_name in loader.disk_migrations:
if _is_ignored(app_label, migration_name, ignores):
continue
if fnmatch(migration_name, '????_auto_*'):
messages.append(
DjangoWarning(
(
f'Migration {app_label}.{migration_name} '
'has an automatic name.'
),
hint=(
'Rename the migration to describe its contents, '
+ "or if it's from a third party app, add to "
+ _SETTINGS_NAME
),
id=f'{CHECK_NAME}.W001',
),
)
return messages
CHECKS: Final = (check_migration_names,)
0707010000002A000081A40000000000000000000000016802260C00000149000000000000000000000000000000000000005500000000django-test-migrations-1.5.0/django_test_migrations/checks/database_configuration.pyfrom typing import Final
from django_test_migrations.db.checks.statement_timeout import (
check_statement_timeout_setting,
)
#: We use this value as a unique identifier of databases related check.
CHECK_NAME: Final = 'django_test_migrations.checks.database_configuration'
CHECKS: Final = (check_statement_timeout_setting,)
0707010000002B000081A40000000000000000000000016802260C00000091000000000000000000000000000000000000004100000000django-test-migrations-1.5.0/django_test_migrations/constants.pyfrom typing import Final
#: marker/tag indicating that marked test is a Django's migration test
MIGRATION_TEST_MARKER: Final = 'migration_test'
0707010000002C000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003C00000000django-test-migrations-1.5.0/django_test_migrations/contrib0707010000002D000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004800000000django-test-migrations-1.5.0/django_test_migrations/contrib/__init__.py0707010000002E000081A40000000000000000000000016802260C0000088E000000000000000000000000000000000000004D00000000django-test-migrations-1.5.0/django_test_migrations/contrib/django_checks.pyfrom django.apps import AppConfig
from django.core import checks
from typing_extensions import final
from django_test_migrations.checks import autonames, database_configuration
@final
class AutoNames(AppConfig):
"""
Class to install this check into ``INSTALLED_APPS`` in ``django``.
If you have migrations that cannot be renamed,
use ``DTM_IGNORED_MIGRATIONS`` setting in ``django.conf``
to ignore ones you have to deal with:
.. code:: python
# settings.py
DTM_IGNORED_MIGRATIONS = {
('main_app', '0004_auto_20191119_2125'),
('dependency_app', '0001_auto_20201110_2100'),
}
To run checks use:
.. code:: bash
python manage.py check --deploy --fail-level WARNING
It will return exit code ``1`` if any violations are found.
This can be easily added into your CI.
See:
https://docs.djangoproject.com/en/3.0/ref/applications/
https://twitter.com/AdamChainz/status/1231895529686208512
"""
#: Part of Django API.
name = autonames.CHECK_NAME
def ready(self) -> None:
"""That's how we register our check when apps are ready."""
for check in autonames.CHECKS:
checks.register(check, checks.Tags.compatibility)
@final
class DatabaseConfiguration(AppConfig):
"""Class to install this check into ``INSTALLED_APPS`` in ``django``.
Database configuration checks are made with aim to help/guide developers
set the most appropriate values for some database settings according to
best practices.
Currently supported database settings:
* statement timeout (timeout queries that execution take too long):
* `postgresql` via `statement_timeout` - https://bit.ly/2ZFjaRM
* `mysql` via `max_execution_time` - https://bit.ly/399TBvk
See:
https://github.com/wemake-services/wemake-django-template/issues/1064
"""
#: Part of Django API.
name = database_configuration.CHECK_NAME
def ready(self) -> None:
"""Register database configuration checks."""
for check in database_configuration.CHECKS:
checks.register(check, checks.Tags.database)
0707010000002F000081A40000000000000000000000016802260C00000D5A000000000000000000000000000000000000004D00000000django-test-migrations-1.5.0/django_test_migrations/contrib/pytest_plugin.pyfrom typing import TYPE_CHECKING, Protocol
import pytest
from django.db import DEFAULT_DB_ALIAS
from django_test_migrations.constants import MIGRATION_TEST_MARKER
if TYPE_CHECKING:
from django_test_migrations.migrator import Migrator
def pytest_load_initial_conftests(early_config: pytest.Config) -> None:
"""Register pytest's markers."""
early_config.addinivalue_line(
'markers',
f"{MIGRATION_TEST_MARKER}: mark the test as a Django's migration test.",
)
def pytest_collection_modifyitems(
session: pytest.Session,
items: list[pytest.Item], # noqa: WPS110
) -> None:
"""
Mark all tests using ``migrator_factory`` fixture with proper marks.
Add ``MIGRATION_TEST_MARKER`` marker to all items using
``migrator_factory`` fixture.
"""
for pytest_item in items:
if 'migrator_factory' in getattr(pytest_item, 'fixturenames', []):
pytest_item.add_marker(MIGRATION_TEST_MARKER)
class MigratorFactory(Protocol):
"""Protocol for `migrator_factory` fixture."""
def __call__(self, database_name: str | None = None) -> 'Migrator':
"""It only has a `__call__` magic method."""
@pytest.fixture
def migrator_factory(
request: pytest.FixtureRequest,
transactional_db: None,
django_db_use_migrations: bool, # noqa: FBT001
) -> MigratorFactory:
"""
Pytest fixture to create migrators inside the pytest tests.
How? Here's an example.
.. code:: python
@pytest.mark.django_db
def test_migration(migrator_factory):
migrator = migrator_factory('custom_db_alias')
old_state = migrator.apply_initial_migration(('main_app', None))
new_state = migrator.apply_tested_migration(
('main_app', '0001_initial'),
)
assert isinstance(old_state, ProjectState)
assert isinstance(new_state, ProjectState)
Why do we import :class:`Migrator` inside the fixture function?
Otherwise, coverage won't work correctly during our internal tests.
Why? Because modules in Python are singletons.
Once imported, they will be stored in memory and reused.
That's why we cannot import ``Migrator`` on a module level.
Because it won't be caught be coverage later on.
"""
from django_test_migrations.migrator import Migrator # noqa: PLC0415
if not django_db_use_migrations:
pytest.skip('--nomigrations was specified')
def factory(database_name: str | None = None) -> Migrator:
migrator = Migrator(database_name)
request.addfinalizer(migrator.reset)
return migrator
return factory
@pytest.fixture
def migrator(migrator_factory: MigratorFactory) -> 'Migrator':
"""
Useful alias for ``'default'`` database in ``django``.
That's a predefined instance of a ``migrator_factory``.
How to use it? Here's an example.
.. code:: python
@pytest.mark.django_db
def test_migration(migrator):
old_state = migrator.apply_initial_migration(('main_app', None))
new_state = migrator.apply_tested_migration(
('main_app', '0001_initial'),
)
assert isinstance(old_state, ProjectState)
assert isinstance(new_state, ProjectState)
Just one step easier than ``migrator_factory`` fixture.
"""
return migrator_factory(DEFAULT_DB_ALIAS)
07070100000030000081A40000000000000000000000016802260C00000AC8000000000000000000000000000000000000004D00000000django-test-migrations-1.5.0/django_test_migrations/contrib/unittest_case.pyfrom typing import Any, ClassVar
import django
from django.db.migrations.state import ProjectState
from django.db.models.signals import post_migrate, pre_migrate
from django.test import TransactionTestCase, tag
from django_test_migrations.constants import MIGRATION_TEST_MARKER
from django_test_migrations.migrator import Migrator
from django_test_migrations.types import MigrationSpec
@tag(MIGRATION_TEST_MARKER)
class MigratorTestCase(TransactionTestCase):
"""Used when using raw ``unitest`` library for test."""
database_name: ClassVar[str | None] = None
old_state: ProjectState
new_state: ProjectState
#: Part of the end-user API. Used to tell what migrations we are using.
migrate_from: ClassVar[MigrationSpec]
migrate_to: ClassVar[MigrationSpec]
# hold original receivers to restore them after each test
_pre_migrate_receivers: list[Any]
_post_migrate_receivers: list[Any]
def setUp(self) -> None:
"""
Regular ``unittest`` styled setup case.
What it does?
- It starts with defining the initial migration state
- Then it allows to run custom method
to prepare some data before the migration will happen
- Then it applies the migration and saves all states
"""
super().setUp()
self._migrator = Migrator(self.database_name)
self.old_state = self._migrator.apply_initial_migration(
self.migrate_from,
)
self.prepare()
self.new_state = self._migrator.apply_tested_migration(self.migrate_to)
def prepare(self) -> None:
"""
Part of the end-user API.
Used to prepare some data before the migration process.
"""
def tearDown(self) -> None:
"""Used to clean mess up after each test."""
pre_migrate.receivers = self._pre_migrate_receivers
post_migrate.receivers = self._post_migrate_receivers
self._migrator.reset()
super().tearDown()
@classmethod
def _store_receivers(cls) -> None:
cls._pre_migrate_receivers, pre_migrate.receivers = ( # noqa: WPS414
pre_migrate.receivers,
[],
)
cls._post_migrate_receivers, post_migrate.receivers = ( # noqa: WPS414
post_migrate.receivers,
[],
)
if django.VERSION[:2] < (5, 2): # noqa: WPS604 # pragma: no cover
def _pre_setup(self) -> None:
self._store_receivers()
super()._pre_setup() # type: ignore[misc]
else: # pragma: no cover
@classmethod
def _pre_setup(cls) -> None: # type: ignore[misc] # noqa: WPS614
cls._store_receivers()
super()._pre_setup() # type: ignore[misc]
07070100000031000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003700000000django-test-migrations-1.5.0/django_test_migrations/db07070100000032000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004300000000django-test-migrations-1.5.0/django_test_migrations/db/__init__.py07070100000033000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004000000000django-test-migrations-1.5.0/django_test_migrations/db/backends07070100000034000081A40000000000000000000000016802260C00000151000000000000000000000000000000000000004C00000000django-test-migrations-1.5.0/django_test_migrations/db/backends/__init__.py# register all ``BaseDatabaseConfiguration`` subclasses
from django_test_migrations.db.backends.mysql.configuration import (
MySQLDatabaseConfiguration as MySQLDatabaseConfiguration,
)
from django_test_migrations.db.backends.postgresql.configuration import (
PostgreSQLDatabaseConfiguration as PostgreSQLDatabaseConfiguration,
)
07070100000035000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004500000000django-test-migrations-1.5.0/django_test_migrations/db/backends/base07070100000036000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000005100000000django-test-migrations-1.5.0/django_test_migrations/db/backends/base/__init__.py07070100000037000081A40000000000000000000000016802260C00000535000000000000000000000000000000000000005600000000django-test-migrations-1.5.0/django_test_migrations/db/backends/base/configuration.pyimport abc
import inspect
from typing import ClassVar
from django_test_migrations.db.backends.exceptions import (
DatabaseConfigurationSettingNotFound,
)
from django_test_migrations.db.backends.registry import (
database_configuration_registry,
)
from django_test_migrations.types import AnyConnection, DatabaseSettingValue
class BaseDatabaseConfiguration(abc.ABC):
"""Interact with database's settings."""
vendor: ClassVar[str]
@classmethod
def __init_subclass__(cls, **kwargs: object) -> None:
"""Register ``BaseDatabaseConfiguration`` subclass of db ``vendor``."""
if not inspect.isabstract(cls):
database_configuration_registry.setdefault(cls.vendor, cls)
def __init__(self, connection: AnyConnection) -> None:
"""Bind database ``connection`` used to retrieve settings values."""
self.connection = connection
@property
@abc.abstractmethod
def statement_timeout(self) -> str:
"""Get `STATEMENT TIMEOUT` setting name."""
@abc.abstractmethod
def get_setting_value(self, name: str) -> DatabaseSettingValue:
"""Retrieve value of ``vendor`` database's ``name`` setting.
Raises:
DatabaseConfigurationSettingNotFound
"""
raise DatabaseConfigurationSettingNotFound(self.vendor, name)
07070100000038000081A40000000000000000000000016802260C00000441000000000000000000000000000000000000004E00000000django-test-migrations-1.5.0/django_test_migrations/db/backends/exceptions.pyfrom typing import ClassVar
class BaseDatabaseConfigurationException(Exception): # noqa: N818
"""Base exception for errors related to database configuration."""
class DatabaseConfigurationNotFound(BaseDatabaseConfigurationException):
"""``BaseDatabaseConfiguration`` subclass when given db vendor not found."""
message_template: ClassVar[str] = (
'``BaseDatabaseConfiguration`` subclass for "{0}" vendor not found'
)
def __init__(self, vendor: str) -> None:
"""Format and set message from args and ``message_template``."""
super().__init__(self.message_template.format(vendor))
class DatabaseConfigurationSettingNotFound(BaseDatabaseConfigurationException):
"""Database configurations setting not found."""
message_template: ClassVar[str] = (
'Database vendor "{0}" does not support setting "{1}"'
)
def __init__(self, vendor: str, setting_name: str) -> None:
"""Format and set message from args and ``message_template``."""
super().__init__(self.message_template.format(vendor, setting_name))
07070100000039000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004600000000django-test-migrations-1.5.0/django_test_migrations/db/backends/mysql0707010000003A000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000005200000000django-test-migrations-1.5.0/django_test_migrations/db/backends/mysql/__init__.py0707010000003B000081A40000000000000000000000016802260C0000051F000000000000000000000000000000000000005700000000django-test-migrations-1.5.0/django_test_migrations/db/backends/mysql/configuration.pyfrom functools import cached_property
from typing import cast
from typing_extensions import final
from django_test_migrations.db.backends.base.configuration import (
BaseDatabaseConfiguration,
)
from django_test_migrations.types import DatabaseSettingValue
@final
class MySQLDatabaseConfiguration(BaseDatabaseConfiguration):
"""Interact with MySQL database configuration."""
vendor = 'mysql'
def get_setting_value(self, name: str) -> DatabaseSettingValue:
"""Retrieve value of MySQL database's setting with ``name``."""
with self.connection.cursor() as cursor:
quoted = self.connection.ops.quote_name(name)
cursor.execute(f'SELECT @@{quoted};')
setting_value = cursor.fetchone()
if not setting_value:
return super().get_setting_value(name)
return cast(DatabaseSettingValue, setting_value[0])
@cached_property
def version(self) -> str:
"""Get MySQL DB server version."""
return str(self.get_setting_value('VERSION'))
@property
def statement_timeout(self) -> str:
"""Get `STATEMENT TIMEOUT` setting name based on DB server version."""
if 'mariadb' in self.version.lower():
return 'MAX_STATEMENT_TIME'
return 'MAX_EXECUTION_TIME'
0707010000003C000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004B00000000django-test-migrations-1.5.0/django_test_migrations/db/backends/postgresql0707010000003D000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000005700000000django-test-migrations-1.5.0/django_test_migrations/db/backends/postgresql/__init__.py0707010000003E000081A40000000000000000000000016802260C0000040E000000000000000000000000000000000000005C00000000django-test-migrations-1.5.0/django_test_migrations/db/backends/postgresql/configuration.pyfrom typing import cast
from typing_extensions import final
from django_test_migrations.db.backends.base.configuration import (
BaseDatabaseConfiguration,
)
from django_test_migrations.types import DatabaseSettingValue
@final
class PostgreSQLDatabaseConfiguration(BaseDatabaseConfiguration):
"""Interact with PostgreSQL database configuration."""
vendor = 'postgresql'
statement_timeout = 'statement_timeout'
def get_setting_value(self, name: str) -> DatabaseSettingValue:
"""Retrieve value of PostgreSQL database's setting with ``name``."""
with self.connection.cursor() as cursor:
cursor.execute(
(
'SELECT setting FROM pg_settings ' # noqa: S608
+ 'WHERE name = %s;'
),
(name,),
)
setting_value = cursor.fetchone()
if not setting_value:
return super().get_setting_value(name)
return cast(DatabaseSettingValue, setting_value[0])
0707010000003F000081A40000000000000000000000016802260C0000048B000000000000000000000000000000000000004C00000000django-test-migrations-1.5.0/django_test_migrations/db/backends/registry.pyfrom collections.abc import MutableMapping
from typing import TYPE_CHECKING
from django_test_migrations.db.backends.exceptions import (
DatabaseConfigurationNotFound,
)
from django_test_migrations.types import AnyConnection
if TYPE_CHECKING:
from django_test_migrations.db.backends.base.configuration import (
BaseDatabaseConfiguration,
)
_DatabaseConfigurationMapping = MutableMapping[
str,
type['BaseDatabaseConfiguration'],
]
database_configuration_registry: _DatabaseConfigurationMapping = {}
def get_database_configuration(
connection: AnyConnection,
) -> 'BaseDatabaseConfiguration':
"""Return proper ``BaseDatabaseConfiguration`` subclass instance.
Raises:
DatabaseConfigurationNotFound
when vendor extracted from ``connection`` doesn't support
interaction with database configuration/settings
"""
vendor = getattr(connection, 'vendor', '')
try:
database_configuration_class = database_configuration_registry[vendor]
except KeyError as exc:
raise DatabaseConfigurationNotFound(vendor) from exc
return database_configuration_class(connection)
07070100000040000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003E00000000django-test-migrations-1.5.0/django_test_migrations/db/checks07070100000041000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004A00000000django-test-migrations-1.5.0/django_test_migrations/db/checks/__init__.py07070100000042000081A40000000000000000000000016802260C00000E11000000000000000000000000000000000000005300000000django-test-migrations-1.5.0/django_test_migrations/db/checks/statement_timeout.pyimport datetime
from typing import Final
from django.core.checks import CheckMessage
from django.core.checks import Warning as DjangoWarning
from django.db import connections
from django_test_migrations.db.backends import exceptions, registry
from django_test_migrations.db.backends.base.configuration import (
BaseDatabaseConfiguration,
)
from django_test_migrations.logic.datetime import timedelta_to_milliseconds
from django_test_migrations.types import AnyConnection
#: We use this value as a unique identifier of databases related check.
CHECK_NAME: Final = 'django_test_migrations.checks.database_configuration'
STATEMENT_TIMEOUT_MINUTES_UPPER_LIMIT: Final = 30
def check_statement_timeout_setting(
*args: object,
**kwargs: object,
) -> list[CheckMessage]:
"""Check if statements' timeout settings is properly configured."""
messages: list[CheckMessage] = []
for connection in connections.all():
_check_statement_timeout_setting(connection, messages)
return messages
def _check_statement_timeout_setting(
connection: AnyConnection,
messages: list[CheckMessage],
) -> None:
try:
database_configuration = registry.get_database_configuration(
connection,
)
except exceptions.DatabaseConfigurationNotFound:
return
try:
setting_value = int(
database_configuration.get_setting_value(
database_configuration.statement_timeout,
)
)
except exceptions.DatabaseConfigurationSettingNotFound:
return
_ensure_statement_timeout_is_set(
database_configuration,
setting_value,
messages,
)
_ensure_statement_timeout_not_too_high(
database_configuration,
setting_value,
messages,
)
def _ensure_statement_timeout_is_set(
database_configuration: BaseDatabaseConfiguration,
setting_value: int,
messages: list[CheckMessage],
) -> None:
if not setting_value:
connection = database_configuration.connection
messages.append(
DjangoWarning(
(
f'{connection.alias}: statement timeout'
' "{database_configuration.statement_timeout}" '
'setting is not set.'
),
hint=(
f'Set "{database_configuration.statement_timeout}" database'
' setting to some reasonable value.'
),
id=f'{CHECK_NAME}.W001',
),
)
def _ensure_statement_timeout_not_too_high(
database_configuration: BaseDatabaseConfiguration,
setting_value: int,
messages: list[CheckMessage],
) -> None:
upper_limit = timedelta_to_milliseconds(
datetime.timedelta(minutes=STATEMENT_TIMEOUT_MINUTES_UPPER_LIMIT),
)
if setting_value > upper_limit:
messages.append(
DjangoWarning(
(
'{0}: statement timeout "{1}" setting value - "{2} ms" '
+ 'might be too high.'
).format(
database_configuration.connection.alias,
database_configuration.statement_timeout,
setting_value,
),
hint=(
'Set "{0}" database setting to some '
+ 'reasonable value, but remember it should not be '
+ 'too high.'
).format(database_configuration.statement_timeout),
id=f'{CHECK_NAME}.W002',
),
)
07070100000043000081A40000000000000000000000016802260C000001DA000000000000000000000000000000000000004200000000django-test-migrations-1.5.0/django_test_migrations/exceptions.pyfrom django_test_migrations.types import MigrationTarget
class MigrationNotInPlan(Exception): # noqa: N818
"""``MigrationTarget`` not found in migrations plan."""
def __init__(self, migration_target: MigrationTarget) -> None: # noqa: D107
self.migration_target = migration_target
def __str__(self) -> str:
"""String representation of exception's instance."""
return f'Migration {self.migration_target} not found in migrations plan'
07070100000044000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003A00000000django-test-migrations-1.5.0/django_test_migrations/logic07070100000045000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004600000000django-test-migrations-1.5.0/django_test_migrations/logic/__init__.py07070100000046000081A40000000000000000000000016802260C000000C0000000000000000000000000000000000000004600000000django-test-migrations-1.5.0/django_test_migrations/logic/datetime.pyimport datetime
def timedelta_to_milliseconds(timedelta: datetime.timedelta) -> int:
"""Convert ``timedelta`` object to milliseconds."""
return int(timedelta.total_seconds() * 1000)
07070100000047000081A40000000000000000000000016802260C00000149000000000000000000000000000000000000004800000000django-test-migrations-1.5.0/django_test_migrations/logic/migrations.pyfrom django_test_migrations.types import MigrationSpec, MigrationTarget
def normalize(migration_target: MigrationSpec) -> list[MigrationTarget]:
"""Normalize ``migration_target`` to expected format."""
if not isinstance(migration_target, list):
migration_target = [migration_target]
return migration_target
07070100000048000081A40000000000000000000000016802260C00000C5B000000000000000000000000000000000000004000000000django-test-migrations-1.5.0/django_test_migrations/migrator.pyfrom django.core.management import call_command
from django.core.management.color import no_style
from django.db import DEFAULT_DB_ALIAS, connections
from django.db.migrations.executor import MigrationExecutor
from django.db.migrations.state import ProjectState
from django_test_migrations import sql
from django_test_migrations.logic.migrations import normalize
from django_test_migrations.plan import truncate_plan
from django_test_migrations.signals import mute_migrate_signals
from django_test_migrations.types import (
MigrationPlan,
MigrationSpec,
MigrationTarget,
)
class Migrator:
"""
Class to manage your migrations and app state.
It is designed to be used inside the tests to ensure that migrations
are working as intended: both data and schema migrations.
This class can be but probably should not be used directly.
Because we have utility test framework
integrations for ``unitest`` and ``pytest``.
Use them for better experience.
"""
def __init__(
self,
database: str | None = None,
) -> None:
"""That's where we initialize all required internals."""
if database is None:
database = DEFAULT_DB_ALIAS
self._database: str = database
self._executor = MigrationExecutor(connections[self._database])
def apply_initial_migration(self, targets: MigrationSpec) -> ProjectState:
"""Reverse back to the original migration."""
migration_targets = normalize(targets)
style = no_style()
# start from clean database state
sql.drop_models_tables(self._database, style)
sql.flush_django_migrations_table(self._database, style)
# prepare as broad plan as possible based on full plan
self._executor.loader.build_graph() # reload
full_plan = self._executor.migration_plan(
self._executor.loader.graph.leaf_nodes(),
clean_start=True,
)
plan = truncate_plan(migration_targets, full_plan)
# apply all migrations from generated plan on clean database
# (only forward, so any unexpected migration won't be applied)
# to restore database state before tested migration
return self._migrate(migration_targets, plan=plan)
def apply_tested_migration(self, targets: MigrationSpec) -> ProjectState:
"""Apply the next migration."""
self._executor.loader.build_graph() # reload
return self._migrate(normalize(targets))
def reset(self) -> None:
"""
Reset the state to the most recent one.
Notably, signals are not muted here to avoid
https://github.com/wemake-services/django-test-migrations/issues/128
"""
call_command('migrate', verbosity=0, database=self._database)
def _migrate(
self,
migration_targets: list[MigrationTarget],
plan: MigrationPlan | None = None,
) -> ProjectState:
with mute_migrate_signals():
project_state = self._executor.migrate(migration_targets, plan=plan)
project_state.clear_delayed_apps_cache()
return project_state
07070100000049000081A40000000000000000000000016802260C00001188000000000000000000000000000000000000003C00000000django-test-migrations-1.5.0/django_test_migrations/plan.pyfrom django.apps import apps
from django.db import DEFAULT_DB_ALIAS, connections
from django.db.migrations import Migration
from django.db.migrations.graph import Node
from django.db.migrations.loader import MigrationLoader
from django_test_migrations.exceptions import MigrationNotInPlan
from django_test_migrations.types import MigrationPlan, MigrationTarget
def all_migrations(
database: str = DEFAULT_DB_ALIAS,
app_names: list[str] | None = None,
) -> list[Node]:
"""
Returns the sorted list of migrations nodes.
The order is the same as when migrations are applied.
When you might need this function?
When you are testing the migration order.
For example, imagine that you have a direct dependency:
``main_app.0002_migration`` and ``other_app.0001_initial``
where ``other_app.0001_initial`` relies on the model or field
introduced in ``main_app.0002_migration``.
You can use ``dependencies`` field
to ensure that everything works correctly.
But, sometimes migrations are squashed,
sometimes they are renamed, refactored, and moved.
It would be better to have a test that will ensure
that ``other_app.0001_initial`` comes after ``main_app.0002_migration``.
And everything works as expected.
"""
loader = MigrationLoader(connections[database])
if app_names:
_validate_app_names(app_names)
targets = [
key for key in loader.graph.leaf_nodes() if key[0] in app_names
]
else:
targets = loader.graph.leaf_nodes()
return _generate_plan(targets, loader)
def nodes_to_tuples(nodes: list[Node]) -> list[tuple[str, str]]:
"""Utility function to transform nodes to tuples."""
return [(node[0], node[1]) for node in nodes]
def _validate_app_names(app_names: list[str]) -> None:
"""
Validates the provided app names.
Raises ```LookupError`` when incorrect app names are provided.
"""
for app_name in app_names:
apps.get_app_config(app_name)
def _generate_plan(
targets: list[Node],
loader: MigrationLoader,
) -> list[Node]:
plan = []
seen: set[Node] = set()
# Generate the plan
for target in targets:
for migration in loader.graph.forwards_plan(target):
if migration not in seen:
node = loader.graph.node_map[migration]
plan.append(node)
seen.add(migration)
return plan
def truncate_plan(
targets: list[MigrationTarget],
plan: MigrationPlan,
) -> MigrationPlan:
"""Truncate migrations ``plan`` up to ``targets``.
This method is used mainly to truncate full/clean migrations plan
to get as broad plan as possible.
By "broad" plan we mean plan with all targets migrations included
as well as all older migrations not related with targets.
"Broad" plan is needed mostly to make ``Migrator`` API developers'
friendly, just to not force developers to include migrations targets
for all other models they want to use in test (e.g. to setup some
model instances) in ``migrate_from``.
Such plan will also produce database state really similar to state
from our production environment just before new migrations are applied.
Migrations plan for targets generated by Django's
``MigrationExecutor.migration_plan`` is minimum plan needed to apply
targets migrations, it includes only migrations targets with all its
dependencies, so it doesn't fit to our approach, that's why following
function is needed.
"""
if not targets or not plan:
return plan
target_max_index = max(_get_index(target, plan) for target in targets)
return plan[: (target_max_index + 1)]
def _get_index(target: MigrationTarget, plan: MigrationPlan) -> int:
try:
index = next(
index
for index, (migration, _) in enumerate(plan)
if _filter_predicate(target, migration)
)
except StopIteration:
raise MigrationNotInPlan(target) from None
return index - (target[1] is None)
def _filter_predicate(target: MigrationTarget, migration: Migration) -> bool:
# when ``None`` passed as migration name then initial migration from
# target's app will be chosen and handled properly in ``_get_index``
# so in final all target app migrations will be excluded from plan
index = 2 - (target[1] is None)
return (migration.app_label, migration.name)[:index] == target[:index]
0707010000004A000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000003D00000000django-test-migrations-1.5.0/django_test_migrations/py.typed0707010000004B000081A40000000000000000000000016802260C00000291000000000000000000000000000000000000003F00000000django-test-migrations-1.5.0/django_test_migrations/signals.pyfrom __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any
from django.db.models.signals import post_migrate, pre_migrate
@contextmanager
def mute_migrate_signals() -> Iterator[tuple[Any, Any]]:
"""Context manager to mute migration-related signals."""
pre_migrate_receivers = pre_migrate.receivers
post_migrate_receivers = post_migrate.receivers
pre_migrate.receivers = []
post_migrate.receivers = []
yield pre_migrate_receivers, post_migrate_receivers
pre_migrate.receivers = pre_migrate_receivers
post_migrate.receivers = post_migrate_receivers
0707010000004C000081A40000000000000000000000016802260C0000065F000000000000000000000000000000000000003B00000000django-test-migrations-1.5.0/django_test_migrations/sql.pyfrom typing import Final
from django.core.management.color import Style, no_style
from django.db import connections
DJANGO_MIGRATIONS_TABLE_NAME: Final = 'django_migrations'
def drop_models_tables(
database_name: str,
style: Style | None = None,
) -> None:
"""Drop all installed Django's models tables."""
style = style or no_style()
connection = connections[database_name]
tables = connection.introspection.django_table_names(
only_existing=True,
include_views=False,
)
sql_drop_tables = [
connection.SchemaEditorClass.sql_delete_table
% {
'table': style.SQL_FIELD(connection.ops.quote_name(table)),
}
for table in tables
]
if sql_drop_tables:
if connection.vendor == 'mysql':
sql_drop_tables = [
'SET FOREIGN_KEY_CHECKS = 0;',
*sql_drop_tables,
'SET FOREIGN_KEY_CHECKS = 1;',
]
connection.ops.execute_sql_flush(sql_drop_tables)
def flush_django_migrations_table(
database_name: str,
style: Style | None = None,
) -> None:
"""Flush `django_migrations` table.
`django_migrations` is not "regular" Django model, so its not returned
by ``ConnectionRouter.get_migratable_models`` which is used e.g. to
implement sequences reset.
"""
connection = connections[database_name]
connection.ops.execute_sql_flush(
connection.ops.sql_flush(
style or no_style(),
[DJANGO_MIGRATIONS_TABLE_NAME],
allow_cascade=False,
reset_sequences=True,
),
)
0707010000004D000081A40000000000000000000000016802260C00000295000000000000000000000000000000000000003D00000000django-test-migrations-1.5.0/django_test_migrations/types.pyfrom typing import Any, TypeAlias, Union
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.migrations import Migration
from django.utils.connection import ConnectionProxy
# Migration target: (app_name, migration_name)
# Regular or rollback migration: 0001 -> 0002, or 0002 -> 0001
# Rollback migration to initial state: 0001 -> None
MigrationTarget: TypeAlias = tuple[str, str | None]
MigrationSpec: TypeAlias = MigrationTarget | list[MigrationTarget]
MigrationPlan: TypeAlias = list[tuple[Migration, bool]]
AnyConnection: TypeAlias = Union['ConnectionProxy[Any]', BaseDatabaseWrapper]
DatabaseSettingValue: TypeAlias = str | int
0707010000004E000081A40000000000000000000000016802260C000005F7000000000000000000000000000000000000003000000000django-test-migrations-1.5.0/docker-compose.yml---
version: '3.8'
services:
postgresql-db:
image: postgres:13
ports:
- "5432:5432"
environment:
- POSTGRES_USER=django
- POSTGRES_PASSWORD=passwd123
- POSTGRES_DB=db
postgresql-db-17:
image: postgres:17
ports:
- "5433:5432"
environment:
- POSTGRES_USER=django
- POSTGRES_PASSWORD=passwd123
- POSTGRES_DB=db
mysql-db:
image: mysql:8
ports:
- "3306:3306"
restart: unless-stopped
environment:
- MYSQL_USER=django
- MYSQL_PASSWORD=passwd123
# NOTE: MySQL container entrypoint gives user `${MYSQL_USER}` access
# only to `${MYSQL_DATABASE}` database, so we are setting
# `${MYSQL_DATABASE}` to Django default test database's name to avoid
# overriding `ENTRYPOINT` or `CMD`.
- MYSQL_DATABASE=test_db
- MYSQL_ROOT_PASSWORD=superpasswd123
command: --default-authentication-plugin=mysql_native_password
maria-db:
image: mariadb:10
ports:
- "3307:3306"
restart: unless-stopped
environment:
- MARIADB_USER=django
- MARIADB_PASSWORD=passwd123
# NOTE: MySQL container entrypoint gives user `${MYSQL_USER}` access
# only to `${MYSQL_DATABASE}` database, so we are setting
# `${MYSQL_DATABASE}` to Django default test database's name to avoid
# overriding `ENTRYPOINT` or `CMD`.
- MARIADB_DATABASE=test_db
- MARIADB_ROOT_PASSWORD=superpasswd123
command: --default-authentication-plugin=mysql_native_password
0707010000004F000081A40000000000000000000000016802260C00009A84000000000000000000000000000000000000002900000000django-test-migrations-1.5.0/poetry.lock# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "asgiref"
version = "3.8.1"
description = "ASGI specs, helper code, and adapters"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
{file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
]
[package.dependencies]
typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
[package.extras]
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]]
name = "attrs"
version = "25.3.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
{file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
]
[package.extras]
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coverage"
version = "7.8.0"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"},
{file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"},
{file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"},
{file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"},
{file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"},
{file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"},
{file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"},
{file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"},
{file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"},
{file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"},
{file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"},
{file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"},
{file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"},
{file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"},
{file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"},
{file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"},
{file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"},
{file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"},
{file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"},
{file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"},
{file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"},
{file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"},
{file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"},
{file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"},
{file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"},
{file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"},
{file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"},
{file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"},
{file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"},
{file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"},
{file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"},
{file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"},
{file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"},
{file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"},
{file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"},
{file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"},
{file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"},
{file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"},
{file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"},
{file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"},
{file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"},
{file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"},
{file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"},
{file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"},
{file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"},
{file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"},
{file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"},
{file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"},
{file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"},
{file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"},
{file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"},
{file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"},
{file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"},
{file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"},
{file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"},
{file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"},
{file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"},
{file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"},
{file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"},
{file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"},
{file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"},
{file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"},
{file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "django"
version = "5.2"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83"},
{file = "Django-5.2.tar.gz", hash = "sha256:1a47f7a7a3d43ce64570d350e008d2949abe8c7e21737b351b6a1611277c6d89"},
]
[package.dependencies]
asgiref = ">=3.8.1"
sqlparse = ">=0.3.1"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-stubs"
version = "5.1.3"
description = "Mypy stubs for Django"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "django_stubs-5.1.3-py3-none-any.whl", hash = "sha256:716758ced158b439213062e52de6df3cff7c586f9f9ad7ab59210efbea5dfe78"},
{file = "django_stubs-5.1.3.tar.gz", hash = "sha256:8c230bc5bebee6da282ba8a27ad1503c84a0c4cd2f46e63d149e76d2a63e639a"},
]
[package.dependencies]
asgiref = "*"
django = "*"
django-stubs-ext = ">=5.1.3"
tomli = {version = "*", markers = "python_version < \"3.11\""}
types-PyYAML = "*"
typing-extensions = ">=4.11.0"
[package.extras]
compatible-mypy = ["mypy (>=1.12,<1.16)"]
oracle = ["oracledb"]
redis = ["redis"]
[[package]]
name = "django-stubs-ext"
version = "5.1.3"
description = "Monkey-patching and extensions for django-stubs"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "django_stubs_ext-5.1.3-py3-none-any.whl", hash = "sha256:64561fbc53e963cc1eed2c8eb27e18b8e48dcb90771205180fe29fc8a59e55fd"},
{file = "django_stubs_ext-5.1.3.tar.gz", hash = "sha256:3e60f82337f0d40a362f349bf15539144b96e4ceb4dbd0239be1cd71f6a74ad0"},
]
[package.dependencies]
django = "*"
typing-extensions = "*"
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "flake8"
version = "7.2.0"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343"},
{file = "flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.13.0,<2.14.0"
pyflakes = ">=3.3.0,<3.4.0"
[[package]]
name = "iniconfig"
version = "2.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy"
version = "1.15.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
]
[package.dependencies]
mypy_extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing_extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
faster-cache = ["orjson"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pycodestyle"
version = "2.13.0"
description = "Python style guide checker"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"},
{file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"},
]
[[package]]
name = "pyflakes"
version = "3.3.2"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a"},
{file = "pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"},
]
[[package]]
name = "pygments"
version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pytest"
version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "6.1.1"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"},
{file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"},
]
[package.dependencies]
coverage = {version = ">=7.5", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytest-django"
version = "4.11.1"
description = "A Django plugin for pytest."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10"},
{file = "pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991"},
]
[package.dependencies]
pytest = ">=7.0.0"
[package.extras]
docs = ["sphinx", "sphinx_rtd_theme"]
testing = ["Django", "django-configurations (>=2.0)"]
[[package]]
name = "pytest-mock"
version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
]
[package.dependencies]
pytest = ">=6.2.5"
[package.extras]
dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]]
name = "pytest-randomly"
version = "3.16.0"
description = "Pytest plugin to randomly order tests and control random.seed."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"},
{file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"},
]
[package.dependencies]
pytest = "*"
[[package]]
name = "ruff"
version = "0.11.6"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1"},
{file = "ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de"},
{file = "ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a"},
{file = "ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193"},
{file = "ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e"},
{file = "ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308"},
{file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55"},
{file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc"},
{file = "ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2"},
{file = "ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6"},
{file = "ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2"},
{file = "ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03"},
{file = "ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b"},
{file = "ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9"},
{file = "ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287"},
{file = "ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e"},
{file = "ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79"},
{file = "ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79"},
]
[[package]]
name = "sqlparse"
version = "0.5.3"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"},
{file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"},
]
[package.extras]
dev = ["build", "hatch"]
doc = ["sphinx"]
[[package]]
name = "tomli"
version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_full_version <= \"3.11.0a6\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20250402"
description = "Typing stubs for PyYAML"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681"},
{file = "types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075"},
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
{file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
]
[[package]]
name = "tzdata"
version = "2025.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"},
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
]
[[package]]
name = "wemake-python-styleguide"
version = "1.1.0"
description = "The strictest and most opinionated python linter ever"
optional = false
python-versions = "<4.0,>=3.10"
groups = ["dev"]
files = [
{file = "wemake_python_styleguide-1.1.0-py3-none-any.whl", hash = "sha256:32644cf35f6cd4c49c2d36e7b10336f8fe105250ba79365e27c5fa648bfc0616"},
{file = "wemake_python_styleguide-1.1.0.tar.gz", hash = "sha256:a9086e4867560c06fe47deb2101c72d1a1fd7ecb7a3235b297b6e02e9298e71e"},
]
[package.dependencies]
attrs = "*"
flake8 = ">=7.1,<8.0"
pygments = ">=2.5,<3.0"
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "6b123052839c20942fd36494e743856fa58e2ee78898334340b629c846ecb251"
07070100000050000081A40000000000000000000000016802260C00000F13000000000000000000000000000000000000002C00000000django-test-migrations-1.5.0/pyproject.toml[build-system]
requires = ["poetry-core>=2.1.0"]
build-backend = "poetry.core.masonry.api"
[project]
name = "django-test-migrations"
version = "1.5.0"
requires-python = ">=3.10,<4.0"
description = "Test django schema and data migrations, including ordering"
license = "MIT"
license-files = [ "LICENSE" ]
authors = [
{name = "sobolevn", email = "mail@sobolevn.me"},
]
readme = "README.md"
repository = "https://github.com/wemake-services/django-test-migrations"
keywords = [
"django",
"django-tests",
"django-migrations",
"django-orm",
"migrations",
"orm",
"sql",
"tests",
"test",
"pytest",
"pytest-plugin"
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Framework :: Django",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
]
dependencies = [
"typing_extensions>=4.0"
]
[project.entry-points."pytest11"]
django_test_migrations = "django_test_migrations.contrib.pytest_plugin"
[tool.poetry.group.dev.dependencies]
django = ">=4.2,<6.0"
mypy = "^1.15"
django-stubs = "^5.1"
wemake-python-styleguide = "^1.1"
ruff = "^0.11"
pytest = "^8.2"
pytest-cov = "^6.0"
pytest-randomly = "^3.15"
pytest-django = "^4.8"
pytest-mock = "^3.14"
[tool.ruff]
# Ruff config: https://docs.astral.sh/ruff/settings
target-version = "py310"
line-length = 80
preview = true
fix = true
format.quote-style = "single"
format.docstring-code-format = false
lint.select = [
"A", # flake8-builtins
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"C90", # maccabe
"COM", # flake8-commas
"D", # pydocstyle
"DTZ", # flake8-datetimez
"E", # pycodestyle
"ERA", # flake8-eradicate
"EXE", # flake8-executable
"F", # pyflakes
"FBT", # flake8-boolean-trap
"FLY", # pyflint
"FURB", # refurb
"G", # flake8-logging-format
"I", # isort
"ICN", # flake8-import-conventions
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"N", # pep8-naming
"PERF", # perflint
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PTH", # flake8-use-pathlib
"PYI", # flake8-pyi
"Q", # flake8-quotes
"RET", # flake8-return
"RSE", # flake8-raise
"RUF", # ruff
"S", # flake8-bandit
"SIM", # flake8-simpify
"SLF", # flake8-self
"SLOT", # flake8-slots
"T100", # flake8-debugger
"TRY", # tryceratops
"UP", # pyupgrade
"W", # pycodestyle
"YTT", # flake8-2020
]
lint.ignore = [
"A005", # allow to shadow stdlib and builtin module names
"COM812", # trailing comma, conflicts with `ruff format`
# Different doc rules that we don't really care about:
"D100",
"D104",
"D106",
"D203",
"D212",
"D401",
"D404",
"D405",
"ISC001", # implicit string concat conflicts with `ruff format`
"ISC003", # prefer explicit string concat over implicit concat
"PLC0414", # it is fine to not rename an import
"PLR09", # we have our own complexity rules
"PLR2004", # do not report magic numbers
"PLR6301", # do not require classmethod / staticmethod when self not used
"TRY003", # long exception messages from `tryceratops`
"RUF012", # mutable class-level defaults are fine
]
lint.external = [ "WPS" ]
lint.flake8-quotes.inline-quotes = "single"
lint.mccabe.max-complexity = 6
lint.pydocstyle.convention = "google"
[tool.ruff.lint.per-file-ignores]
"tests/*.py" = [
"S101", # asserts
"S404", # subprocess calls are for tests
"S603", # do not require `shell=True`
"S607", # partial executable paths
]
07070100000051000081A40000000000000000000000016802260C00000847000000000000000000000000000000000000002700000000django-test-migrations-1.5.0/setup.cfg# All configuration for plugins and other utils is defined here.
# Read more about `setup.cfg`:
# https://docs.python.org/3/distutils/configfile.html
[flake8]
format = wemake
show-source = true
doctests = true
statistics = false
select = WPS, E999
extend-exclude =
.venv
build
per-file-ignores =
django_test_migrations/db/backends/__init__.py: WPS412
django_test_app/main_app/migrations/*.py: WPS102, WPS114, WPS432
django_test_app/django_test_app/settings.py: WPS407
tests/test_*.py: WPS118, WPS226, WPS432
[tool:pytest]
# Django options:
# https://pytest-django.readthedocs.io/en/latest/
DJANGO_SETTINGS_MODULE = django_test_app.settings
# PYTHONPATH configuration:
pythonpath = django_test_app
# py.test options:
norecursedirs =
*.egg
.eggs
dist
build
docs
.tox
.git
__pycache__
# Strict `@xfail` by default:
xfail_strict = true
# You will need to measure your tests speed with `-n auto` and without it,
# so you can see whether it gives you any performance gain, or just gives
# you an overhead. See `docs/template/development-process.rst`.
addopts =
--strict
--doctest-modules
--cov=django_test_migrations
--cov-report=term-missing:skip-covered
--cov-report=html
--cov-report=xml
--cov-branch
--cov-fail-under=100
[coverage:run]
# Why do we exclude this file from coverage?
# Because coverage is not calculated correctly for pytest plugins.
# And we completely test it anyway.
omit =
django_test_migrations/constants.py
django_test_migrations/contrib/pytest_plugin.py
django_test_migrations/types.py
[coverage:report]
skip_covered = True
show_missing = True
sort = Cover
exclude_lines =
pragma: no cover
# type hinting related code
if TYPE_CHECKING:
[mypy]
# mypy configurations: http://bit.ly/2zEl9WI
enable_error_code =
truthy-bool,
truthy-iterable,
redundant-expr,
unused-awaitable,
# ignore-without-code,
possibly-undefined,
redundant-self,
# explicit-override,
# mutable-override,
unimported-reveal,
deprecated,
ignore_missing_imports = true
strict = true
warn_unreachable = true
local_partial_types = true
07070100000052000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002300000000django-test-migrations-1.5.0/tests07070100000053000081A40000000000000000000000016802260C00000190000000000000000000000000000000000000002F00000000django-test-migrations-1.5.0/tests/conftest.pyimport pytest
@pytest.fixture(scope='session')
def django_db_use_migrations(
request,
django_db_use_migrations,
):
"""
Helper fixture to skip tests when ``--nomigrations`` were specified.
Copied from https://github.com/pytest-dev/pytest-django
"""
if not django_db_use_migrations:
pytest.skip('--nomigrations was specified')
return django_db_use_migrations
07070100000054000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002F00000000django-test-migrations-1.5.0/tests/test_checks07070100000055000081A40000000000000000000000016802260C000005EC000000000000000000000000000000000000004100000000django-test-migrations-1.5.0/tests/test_checks/test_autonames.pyimport pytest
from django.core.checks import WARNING
from django_test_migrations.checks.autonames import (
CHECK_NAME,
check_migration_names,
)
@pytest.mark.django_db
def test_autonames():
"""Here we check that bad migrations do produce warnings."""
warnings = check_migration_names()
warnings_msgs = {warning.msg for warning in warnings}
assert len(warnings) == 2
assert [warnings[0].level, warnings[1].level] == [WARNING, WARNING]
assert all(
[
warnings[0].id.startswith(CHECK_NAME),
warnings[1].id.startswith(CHECK_NAME),
],
)
assert warnings_msgs == {
'Migration main_app.0004_auto_20191119_2125 has an automatic name.',
'Migration main_app.0005_auto_20200329_1118 has an automatic name.',
}
@pytest.mark.django_db
def test_autonames_with_ignore(settings):
"""Here we check that some migrations can be ignored."""
# patch settings to ignore two bad migrations
settings.DTM_IGNORED_MIGRATIONS = {
('main_app', '0004_auto_20191119_2125'),
('main_app', '0005_auto_20200329_1118'),
}
warnings = check_migration_names()
assert not warnings
@pytest.mark.django_db
def test_autonames_with_ignore_all_app_migrations(settings):
"""Here we check that all migrations ignored inside app."""
# patch settings to ignore all migrations in the app
settings.DTM_IGNORED_MIGRATIONS = {('main_app', '*')}
warnings = check_migration_names()
assert not warnings
07070100000056000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003000000000django-test-migrations-1.5.0/tests/test_contrib07070100000057000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004300000000django-test-migrations-1.5.0/tests/test_contrib/test_django_checks07070100000058000081A40000000000000000000000016802260C000001A8000000000000000000000000000000000000005B00000000django-test-migrations-1.5.0/tests/test_contrib/test_django_checks/test_autonames_check.pyimport subprocess
def test_managepy_check():
"""Checks that checks do fail."""
proc = subprocess.Popen(
[
'python',
'django_test_app/manage.py',
'check',
'--fail-level',
'WARNING',
],
stderr=subprocess.STDOUT,
universal_newlines=True,
encoding='utf8',
)
proc.communicate()
assert proc.returncode == 1
07070100000059000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004300000000django-test-migrations-1.5.0/tests/test_contrib/test_pytest_plugin0707010000005A000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004F00000000django-test-migrations-1.5.0/tests/test_contrib/test_pytest_plugin/__init__.py0707010000005B000081A40000000000000000000000016802260C000006E7000000000000000000000000000000000000005200000000django-test-migrations-1.5.0/tests/test_contrib/test_pytest_plugin/test_plugin.pyimport re
import subprocess
from django_test_migrations.constants import MIGRATION_TEST_MARKER
def test_call_pytest_setup_plan():
"""Checks that module is registered and visible in the meta data."""
output_text = subprocess.check_output(
[
'pytest',
'--setup-plan',
# We need this part because otherwise check fails with `1` code:
'--cov-fail-under',
'0',
],
stderr=subprocess.STDOUT,
universal_newlines=True,
encoding='utf8',
)
assert 'migrator' in output_text
assert 'migrator_factory' in output_text
def test_pytest_registers_marker():
"""Ensure ``MIGRATION_TEST_MARKER`` marker is registered."""
output_text = subprocess.check_output(
['pytest', '--markers'],
stderr=subprocess.STDOUT,
universal_newlines=True,
encoding='utf8',
)
assert MIGRATION_TEST_MARKER in output_text
def test_pytest_markers():
"""Ensure ``MIGRATION_TEST_MARKER`` markers are properly added."""
output_text = subprocess.check_output(
[
'pytest',
'--collect-only',
# Collect only tests marked with ``MIGRATION_TEST_MARKER`` marker
'-m',
MIGRATION_TEST_MARKER,
# We need this part because otherwise check fails with `1` code:
'--cov-fail-under',
'0',
],
stderr=subprocess.STDOUT,
universal_newlines=True,
encoding='utf8',
)
search_result = re.search(
r'(?P<selected_number>\d+)\s+selected',
output_text,
)
assert search_result
assert int(search_result.group('selected_number') or 0) > 0
assert 'test_pytest_plugin' in output_text
0707010000005C000081A40000000000000000000000016802260C00000B31000000000000000000000000000000000000006000000000django-test-migrations-1.5.0/tests/test_contrib/test_pytest_plugin/test_pytest_plugin_direct.py"""
This module covers simple direct migrations.
We test both schema and data-migrations here.
"""
import pytest
from django.core.exceptions import FieldError
from django.db.utils import IntegrityError
@pytest.mark.django_db
def test_pytest_plugin_initial(migrator):
"""Ensures that the initial migration works."""
old_state = migrator.apply_initial_migration(('main_app', None))
with pytest.raises(LookupError):
# Models does not yet exist:
old_state.apps.get_model('main_app', 'SomeItem')
new_state = migrator.apply_tested_migration(('main_app', '0001_initial'))
# After the initial migration is done, we can use the model state:
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.filter(string_field='').count() == 0
@pytest.mark.django_db
def test_pytest_plugin0001(migrator):
"""Ensures that the first migration works."""
old_state = migrator.apply_initial_migration(('main_app', '0001_initial'))
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
with pytest.raises(FieldError):
SomeItem.objects.filter(is_clean=True)
new_state = migrator.apply_tested_migration(
('main_app', '0002_someitem_is_clean'),
)
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.filter(is_clean=True).count() == 0
@pytest.mark.django_db
def test_pytest_plugin0002(migrator):
"""Ensures that the second migration works."""
old_state = migrator.apply_initial_migration(
('main_app', '0002_someitem_is_clean'),
)
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
SomeItem.objects.create(string_field='a')
SomeItem.objects.create(string_field='a b')
assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 2
new_state = migrator.apply_tested_migration(
('main_app', '0003_update_is_clean'),
)
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 1
@pytest.mark.django_db
def test_pytest_plugin0003(migrator):
"""Ensures that the third migration works."""
old_state = migrator.apply_initial_migration(
('main_app', '0003_update_is_clean'),
)
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
SomeItem.objects.create(string_field='a') # default is still there
assert SomeItem.objects.count() == 1
assert SomeItem.objects.filter(is_clean=True).count() == 1
new_state = migrator.apply_tested_migration(
('main_app', '0004_auto_20191119_2125'),
)
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
with pytest.raises(IntegrityError):
SomeItem.objects.create(string_field='b') # no default anymore
0707010000005D000081A40000000000000000000000016802260C00000623000000000000000000000000000000000000006100000000django-test-migrations-1.5.0/tests/test_contrib/test_pytest_plugin/test_pytest_plugin_reverse.py"""
This module covers tests for migration rollbacks.
It might be useful when something goes wrong
and you need to switch back to the previous state.
"""
import pytest
from django.core.exceptions import FieldError
@pytest.mark.django_db
def test_pytest_plugin0001(migrator):
"""Ensures that the first migration works."""
old_state = migrator.apply_initial_migration(
('main_app', '0002_someitem_is_clean'),
)
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.filter(is_clean=True).count() == 0
new_state = migrator.apply_tested_migration(('main_app', '0001_initial'))
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
with pytest.raises(FieldError):
SomeItem.objects.filter(is_clean=True)
@pytest.mark.django_db
def test_pytest_plugin0002(migrator):
"""Ensures that the second migration works."""
old_state = migrator.apply_initial_migration(
('main_app', '0003_update_is_clean'),
)
SomeItem = old_state.apps.get_model('main_app', 'SomeItem')
SomeItem.objects.create(string_field='a', is_clean=True)
SomeItem.objects.create(string_field='a b', is_clean=False)
assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 1
new_state = migrator.apply_tested_migration(
('main_app', '0002_someitem_is_clean'),
)
SomeItem = new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 1
0707010000005E000081A40000000000000000000000016802260C00000E8D000000000000000000000000000000000000005300000000django-test-migrations-1.5.0/tests/test_contrib/test_pytest_plugin/test_signals.pyfrom typing import Final
import django
import pytest
from django.apps import apps
from django.core.management import call_command
from django.db import DEFAULT_DB_ALIAS
from django.db.models.signals import post_migrate, pre_migrate
from django_test_migrations.signals import mute_migrate_signals
# value for ``dispatch_uid`` is needed to disconnect signal receiver
# registered for testing purposes to which we do not have any reference
# outside of test function
DISPATCH_UID: Final = 'test_migrate_signals'
# Dummy signal receiver function
def _my_callback(sender, **kwargs):
"""Mock that does nothing."""
@pytest.fixture
def _disconnect_receivers():
"""Disconnect testing receiver of ``pre_migrate`` or ``post_migrate``."""
yield
main_app_config = apps.get_app_config('main_app')
pre_migrate.disconnect(sender=main_app_config, dispatch_uid=DISPATCH_UID)
post_migrate.disconnect(sender=main_app_config, dispatch_uid=DISPATCH_UID)
@pytest.mark.parametrize('signal', [pre_migrate, post_migrate])
def test_migrate_signal_muted(signal):
"""Ensure the context manager does indeed silences the signals."""
signal.connect(_my_callback)
assert signal.receivers
with mute_migrate_signals():
assert not signal.receivers
assert signal.receivers
signal.disconnect(_my_callback)
@pytest.mark.skipif(
django.VERSION >= (4, 0),
reason='requires `Django<4.0`',
)
@pytest.mark.parametrize('signal', [pre_migrate, post_migrate])
@pytest.mark.usefixtures('migrator', '_disconnect_receivers')
def test_signal_receiver_registered_in_test(mocker, signal):
"""Ensure migration signal receivers registered in tests are called."""
signal_receiver_mock = mocker.MagicMock()
main_app_config = apps.get_app_config('main_app')
signal.connect(
signal_receiver_mock,
sender=main_app_config,
dispatch_uid=DISPATCH_UID,
)
verbosity = 0
interactive = False
# call `migrate` management command to trigger ``pre_migrate`` and
# ``post_migrate`` signals
call_command('migrate', verbosity=verbosity, interactive=interactive)
signal_receiver_mock.assert_called_once_with(
sender=main_app_config,
app_config=main_app_config,
apps=mocker.ANY, # we don't have any reference to this object
using=DEFAULT_DB_ALIAS,
verbosity=verbosity,
interactive=interactive,
plan=mocker.ANY, # not important for this test
signal=signal,
)
@pytest.mark.skipif(
django.VERSION < (4, 0),
reason='requires `Django>=4.0`',
)
@pytest.mark.parametrize('signal', [pre_migrate, post_migrate])
@pytest.mark.usefixtures('migrator', '_disconnect_receivers')
def test_signal_receiver_registered_in_test_django40(mocker, signal):
"""Ensure migration signal receivers registered in tests are called."""
signal_receiver_mock = mocker.MagicMock()
main_app_config = apps.get_app_config('main_app')
signal.connect(
signal_receiver_mock,
sender=main_app_config,
dispatch_uid=DISPATCH_UID,
)
verbosity = 0
interactive = False
# call `migrate` management command to trigger ``pre_migrate`` and
# ``post_migrate`` signals
call_command('migrate', verbosity=verbosity, interactive=interactive)
signal_receiver_mock.assert_called_once_with(
sender=main_app_config,
app_config=main_app_config,
apps=mocker.ANY, # we don't have any reference to this object
using=DEFAULT_DB_ALIAS,
verbosity=verbosity,
interactive=interactive,
signal=signal,
# following kwargs are not important for this test
stdout=mocker.ANY,
plan=mocker.ANY,
)
0707010000005F000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004300000000django-test-migrations-1.5.0/tests/test_contrib/test_unittest_case07070100000060000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000004F00000000django-test-migrations-1.5.0/tests/test_contrib/test_unittest_case/__init__.py07070100000061000081A40000000000000000000000016802260C00001234000000000000000000000000000000000000005300000000django-test-migrations-1.5.0/tests/test_contrib/test_unittest_case/test_signals.pyfrom unittest import mock
import django
import pytest
from django.apps import apps
from django.core.management import call_command
from django.db import DEFAULT_DB_ALIAS
from django.db.models.signals import post_migrate, pre_migrate
from django_test_migrations.contrib.unittest_case import MigratorTestCase
class TestSignalMuting(MigratorTestCase):
"""Test that the `post_migrate` signal has been muted."""
migrate_from = ('main_app', '0002_someitem_is_clean')
migrate_to = ('main_app', '0001_initial')
def test_pre_migrate_muted(self):
"""Ensure ``pre_migrate`` signal has been muted."""
assert not pre_migrate.receivers
def test_post_migrate_muted(self):
"""Ensure ``post_migrate`` signal has been muted."""
assert not post_migrate.receivers
class TestSignalConnectInTest(MigratorTestCase):
"""Ensure test ``pre_migrate`` or ``post_migrate`` receiver are called.
Ensure that ``pre_migrate`` or ``post_migrate`` signals receivers
connected directly in tests are called.
"""
migrate_from = ('main_app', '0001_initial')
migrate_to = ('main_app', '0002_someitem_is_clean')
def tearDown(self):
"""Disconnect ``pre_migrate`` and ``post_migrate`` testing receivers."""
pre_migrate.disconnect(
self.pre_migrate_receiver_mock,
sender=self.main_app_config,
)
post_migrate.disconnect(
self.post_migrate_receiver_mock,
sender=self.main_app_config,
)
def prepare(self):
"""Connect testing ``pre_migrate`` and ``post_migrate`` receivers."""
self.pre_migrate_receiver_mock = mock.MagicMock()
self.post_migrate_receiver_mock = mock.MagicMock()
# ``old_apps`` is not real ``ProjectState`` instance, so we cannot use
# it to get "original" main_app ``AppConfig`` instance needed to
# connect signal receiver, that's the reason we are using
# ``apps`` imported directly from ``django.apps``
self.main_app_config = apps.get_app_config('main_app')
pre_migrate.connect(
self.pre_migrate_receiver_mock,
sender=self.main_app_config,
)
post_migrate.connect(
self.post_migrate_receiver_mock,
sender=self.main_app_config,
)
@pytest.mark.skipif(
django.VERSION >= (4, 0),
reason='requires `Django<4.0`',
)
def test_signal_receivers_added_in_tests(self):
"""Ensure migration signals receivers connected in tests are called."""
verbosity = 0
interactive = False
# call `migrate` management command to trigger ``pre_migrate`` and
# ``post_migrate`` signals
call_command('migrate', verbosity=verbosity, interactive=interactive)
common_kwargs = {
'sender': self.main_app_config,
'app_config': self.main_app_config,
'apps': mock.ANY, # we don't have any reference to this object
'using': DEFAULT_DB_ALIAS,
'verbosity': verbosity,
'interactive': interactive,
'plan': mock.ANY, # not important for this test
}
self.pre_migrate_receiver_mock.assert_called_once_with(
**common_kwargs,
signal=pre_migrate,
)
self.post_migrate_receiver_mock.assert_called_once_with(
**common_kwargs,
signal=post_migrate,
)
@pytest.mark.skipif(
django.VERSION < (4, 0),
reason='requires `Django>=4.0`',
)
def test_signal_receivers_added_in_tests_django40(self):
"""Ensure migration signals receivers connected in tests are called."""
verbosity = 0
interactive = False
# call `migrate` management command to trigger ``pre_migrate`` and
# ``post_migrate`` signals
call_command('migrate', verbosity=verbosity, interactive=interactive)
common_kwargs = {
'sender': self.main_app_config,
'app_config': self.main_app_config,
'apps': mock.ANY, # we don't have any reference to this object
'using': DEFAULT_DB_ALIAS,
'verbosity': verbosity,
'interactive': interactive,
# following kwargs are not important for this test
'stdout': mock.ANY,
'plan': mock.ANY,
}
self.pre_migrate_receiver_mock.assert_called_once_with(
**common_kwargs,
signal=pre_migrate,
)
self.post_migrate_receiver_mock.assert_called_once_with(
**common_kwargs,
signal=post_migrate,
)
07070100000062000081A40000000000000000000000016802260C00000768000000000000000000000000000000000000005900000000django-test-migrations-1.5.0/tests/test_contrib/test_unittest_case/test_unittest_case.pyfrom django_test_migrations.constants import MIGRATION_TEST_MARKER
from django_test_migrations.contrib.unittest_case import MigratorTestCase
class TestDirectMigration(MigratorTestCase):
"""This class is used to test direct migrations."""
migrate_from = ('main_app', '0002_someitem_is_clean')
migrate_to = ('main_app', '0003_update_is_clean')
@classmethod
def setUpClass(cls):
"""Override parent's setUpClass to trigger #503."""
super().setUpClass()
def prepare(self):
"""Prepare some data before the migration."""
SomeItem = self.old_state.apps.get_model('main_app', 'SomeItem')
SomeItem.objects.create(string_field='a')
SomeItem.objects.create(string_field='a b')
def test_migration_main0003(self):
"""Run the test itself."""
SomeItem = self.new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.count() == 2
assert SomeItem.objects.filter(is_clean=True).count() == 1
class TestBackwardMigration(MigratorTestCase):
"""This class is used to test backward migrations."""
migrate_from = ('main_app', '0002_someitem_is_clean')
migrate_to = ('main_app', '0001_initial')
def prepare(self):
"""Prepare some data before the migration."""
SomeItem = self.old_state.apps.get_model('main_app', 'SomeItem')
SomeItem.objects.create(string_field='a')
SomeItem.objects.create(string_field='a b')
def test_migration_main0001(self):
"""Run the test itself."""
SomeItem = self.new_state.apps.get_model('main_app', 'SomeItem')
assert SomeItem.objects.count() == 2
def test_migration_test_marker_tag():
"""Ensure ``MigratorTestCase`` subclasses are properly tagged."""
assert MIGRATION_TEST_MARKER in TestDirectMigration.tags
assert MIGRATION_TEST_MARKER in TestBackwardMigration.tags
07070100000063000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002B00000000django-test-migrations-1.5.0/tests/test_db07070100000064000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003900000000django-test-migrations-1.5.0/tests/test_db/test_backends07070100000065000081A40000000000000000000000016802260C00000290000000000000000000000000000000000000004C00000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_exceptions.pyfrom django_test_migrations.db.backends import exceptions
def test_database_configuration_not_found():
"""Ensure exception returns proper string representation."""
vendor = 'ms_sql'
exception = exceptions.DatabaseConfigurationNotFound(vendor)
assert vendor in str(exception)
def test_database_configuration_setting_not_found():
"""Ensure exception returns proper string representation."""
vendor = 'ms_sql'
setting_name = 'fake_setting'
exception = exceptions.DatabaseConfigurationSettingNotFound(
vendor,
setting_name,
)
assert vendor in str(exception)
assert setting_name in str(exception)
07070100000066000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004400000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_mysql07070100000067000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000005000000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_mysql/__init__.py07070100000068000081A40000000000000000000000016802260C00000A64000000000000000000000000000000000000005A00000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_mysql/test_configuration.pyimport pytest
from pytest_mock import MockerFixture
from django_test_migrations.db.backends import mysql
from django_test_migrations.db.backends.exceptions import (
DatabaseConfigurationSettingNotFound,
)
@pytest.mark.parametrize(
('version', 'setting_name'),
[
('8.0.33', 'MAX_EXECUTION_TIME'),
('10.11.2-MariaDB', 'MAX_STATEMENT_TIME'),
('10.11.2-MariaDB-1:10.11.2+maria~ubu2204', 'MAX_STATEMENT_TIME'),
],
)
def test_statement_timeout(
mocker: MockerFixture,
version: str,
setting_name: str,
) -> None:
"""Ensure expected setting name is returned."""
connection_mock = mocker.MagicMock()
cursor_mock = connection_mock.cursor().__enter__() # noqa: PLC2801
cursor_mock.fetchone.return_value = (version,)
database_configuration = mysql.configuration.MySQLDatabaseConfiguration(
connection_mock,
)
assert database_configuration.statement_timeout == setting_name
def test_get_setting_value(mocker: MockerFixture) -> None:
"""Ensure expected SQL query is executed."""
setting_name = 'MAX_EXECUTION_TIME'
connection_mock = mocker.MagicMock()
connection_mock.ops.quote_name = lambda name: name
database_configuration = mysql.configuration.MySQLDatabaseConfiguration(
connection_mock,
)
database_configuration.get_setting_value(setting_name)
cursor_mock = connection_mock.cursor().__enter__() # noqa: PLC2801
cursor_mock.execute.assert_called_once_with(
f'SELECT @@{setting_name};',
)
def test_get_existing_setting_value(mocker: MockerFixture) -> None:
"""Ensure setting value is returned for existing setting."""
expected_setting_value = 74747
connection_mock = mocker.MagicMock()
cursor_mock = connection_mock.cursor().__enter__() # noqa: PLC2801
cursor_mock.fetchone.return_value = (expected_setting_value,)
database_configuration = mysql.configuration.MySQLDatabaseConfiguration(
connection_mock,
)
setting_value = database_configuration.get_setting_value('testing_setting')
assert setting_value == expected_setting_value
def test_get_not_existing_setting_value(mocker: MockerFixture) -> None:
"""Ensure exception is raised when setting does not exist."""
connection_mock = mocker.MagicMock()
cursor_mock = connection_mock.cursor().__enter__() # noqa: PLC2801
cursor_mock.fetchone.return_value = None
database_configuration = mysql.configuration.MySQLDatabaseConfiguration(
connection_mock,
)
with pytest.raises(DatabaseConfigurationSettingNotFound):
database_configuration.get_setting_value('testing_setting')
07070100000069000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000004900000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_postgresql0707010000006A000081A40000000000000000000000016802260C00000000000000000000000000000000000000000000005500000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_postgresql/__init__.py0707010000006B000081A40000000000000000000000016802260C000007A6000000000000000000000000000000000000005F00000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_postgresql/test_configuration.pyimport pytest
from django_test_migrations.db.backends import postgresql
from django_test_migrations.db.backends.exceptions import (
DatabaseConfigurationSettingNotFound,
)
def test_get_setting_value(mocker):
"""Ensure expected SQL query is executed."""
setting_name = 'statement_timeout'
connection_mock = mocker.MagicMock()
connection_mock.ops.quote_name = lambda name: name
database_configuration = (
postgresql.configuration.PostgreSQLDatabaseConfiguration(
connection_mock,
)
)
database_configuration.get_setting_value(setting_name)
cursor_mock = connection_mock.cursor().__enter__() # noqa: PLC2801
cursor_mock.execute.assert_called_once_with(
'SELECT setting FROM pg_settings WHERE name = %s;',
(setting_name,),
)
def test_get_existing_setting_value(mocker):
"""Ensure setting value is returned for existing setting."""
expected_setting_value = 74747
connection_mock = mocker.MagicMock()
cursor_mock = connection_mock.cursor().__enter__() # noqa: PLC2801
cursor_mock.fetchone.return_value = (expected_setting_value,)
database_configuration = (
postgresql.configuration.PostgreSQLDatabaseConfiguration(
connection_mock,
)
)
setting_value = database_configuration.get_setting_value('testing_setting')
assert setting_value == expected_setting_value
def test_get_not_existing_setting_value(mocker):
"""Ensure exception is raised when setting does not exist."""
connection_mock = mocker.MagicMock()
cursor_mock = connection_mock.cursor().__enter__() # noqa: PLC2801
cursor_mock.fetchone.return_value = None
database_configuration = (
postgresql.configuration.PostgreSQLDatabaseConfiguration(
connection_mock,
)
)
with pytest.raises(DatabaseConfigurationSettingNotFound):
database_configuration.get_setting_value('testing_setting')
0707010000006C000081A40000000000000000000000016802260C00000842000000000000000000000000000000000000004A00000000django-test-migrations-1.5.0/tests/test_db/test_backends/test_registry.pyimport pytest
from django_test_migrations.db.backends import mysql, postgresql, registry
from django_test_migrations.db.backends.base.configuration import (
BaseDatabaseConfiguration,
)
from django_test_migrations.db.backends.exceptions import (
DatabaseConfigurationNotFound,
)
def test_all_db_backends_registered():
"""Ensures all database backends all registered."""
registered_vendors = list(registry.database_configuration_registry.keys())
assert sorted(registered_vendors) == ['mysql', 'postgresql']
def test_abc_subclasses_are_not_registered():
"""Test registration of ``BaseDatabaseConfiguration`` abstract subclasses.
Ensures ``BaseDatabaseConfiguration`` abstract subclasses are not
registered.
"""
vendor = 'abstract_subclass'
# creates abstract subclass
type(
'DatabaseConfiguration',
(BaseDatabaseConfiguration,),
{
'vendor': vendor,
},
)
assert vendor not in registry.database_configuration_registry
@pytest.mark.parametrize(
('vendor', 'database_configuration_class'),
[
(
'postgresql',
postgresql.configuration.PostgreSQLDatabaseConfiguration,
),
('mysql', mysql.configuration.MySQLDatabaseConfiguration),
],
)
def test_get_database_configuration_vendor_registered(
mocker,
vendor,
database_configuration_class,
):
"""Ensures database configuration is returned when vendor registered."""
connection_mock = mocker.Mock()
connection_mock.vendor = vendor
database_configuration = registry.get_database_configuration(
connection_mock,
)
assert isinstance(database_configuration, database_configuration_class)
def test_get_database_configuration_vendor_not_registered(mocker):
"""Ensures proper exception is raised when vendor not registered."""
vendor = 'not_registered_vendor'
connection_mock = mocker.Mock()
connection_mock.vendor = vendor
with pytest.raises(DatabaseConfigurationNotFound, match=vendor):
registry.get_database_configuration(connection_mock)
0707010000006D000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003700000000django-test-migrations-1.5.0/tests/test_db/test_checks0707010000006E000081A40000000000000000000000016802260C00000E97000000000000000000000000000000000000005700000000django-test-migrations-1.5.0/tests/test_db/test_checks/test_statement_timeout_check.pyimport datetime
import pytest
from django_test_migrations.db.checks.statement_timeout import (
CHECK_NAME,
check_statement_timeout_setting,
)
from django_test_migrations.logic.datetime import timedelta_to_milliseconds
ALL_CONNECTIONS_MOCK_PATH = (
'django_test_migrations.db.checks.statement_timeout.connections.all'
)
@pytest.fixture
def connection_mock_factory(mocker):
"""Factory of DB connection mocks."""
def factory(vendor, fetch_one_result=None):
connection_mock = mocker.MagicMock(vendor=vendor)
cursor_mock = connection_mock.cursor.return_value
cursor_mock = cursor_mock.__enter__.return_value
cursor_mock.fetchone.return_value = fetch_one_result
return connection_mock
return factory
@pytest.mark.parametrize('vendor', ['postgresql', 'mysql'])
def test_correct_statement_timeout(mocker, connection_mock_factory, vendor):
"""Ensure empty list returned when ``statement_timeout`` value correct."""
connection_mock = connection_mock_factory(vendor, (20000,))
mocker.patch(ALL_CONNECTIONS_MOCK_PATH, return_value=[connection_mock])
assert not check_statement_timeout_setting()
@pytest.mark.parametrize('vendor', ['postgresql', 'mysql'])
def test_statement_timeout_not_set(mocker, connection_mock_factory, vendor):
"""Ensure W001 is returned in list when ``statement_timeout`` not set."""
connection_mock = connection_mock_factory(vendor, (0,))
mocker.patch(ALL_CONNECTIONS_MOCK_PATH, return_value=[connection_mock])
check_messages = check_statement_timeout_setting()
assert len(check_messages) == 1
assert check_messages[0].id.endswith('W001')
@pytest.mark.parametrize('vendor', ['postgresql', 'mysql'])
def test_statement_timeout_too_high(mocker, connection_mock_factory, vendor):
"""Ensure W002 is returned in list when ``statement_timeout`` too high."""
connection_mock = connection_mock_factory(
vendor,
(timedelta_to_milliseconds(datetime.timedelta(hours=2)),),
)
mocker.patch(ALL_CONNECTIONS_MOCK_PATH, return_value=[connection_mock])
check_messages = check_statement_timeout_setting()
assert len(check_messages) == 1
assert check_messages[0].id.endswith('W002')
def test_unsupported_vendors(mocker):
"""Ensure empty list returned when no connections vendors supported."""
vendors = ['sqlite3', 'custom']
connection_mocks = [mocker.MagicMock(vendor=vendor) for vendor in vendors]
mocker.patch(ALL_CONNECTIONS_MOCK_PATH, return_value=connection_mocks)
assert not check_statement_timeout_setting()
@pytest.mark.parametrize('vendor', ['postgresql', 'mysql'])
def test_statement_timeout_setting_not_found(
mocker,
connection_mock_factory,
vendor,
):
"""Ensure empty list returned when ``statement_timeout`` not found."""
connection_mock = connection_mock_factory(vendor, None)
mocker.patch(ALL_CONNECTIONS_MOCK_PATH, return_value=[connection_mock])
assert not check_statement_timeout_setting()
def test_multiple_connections(mocker, connection_mock_factory):
"""Ensure list with many items returned when many connections present."""
connections_mocks = [
connection_mock_factory('sqlite', None),
connection_mock_factory('postgresql', (0,)),
connection_mock_factory(
'mysql',
(timedelta_to_milliseconds(datetime.timedelta(hours=2)),),
),
]
mocker.patch(ALL_CONNECTIONS_MOCK_PATH, return_value=connections_mocks)
check_messages = check_statement_timeout_setting()
expected_messages_ids = [
f'{CHECK_NAME}.W001',
f'{CHECK_NAME}.W002',
]
assert expected_messages_ids == [message.id for message in check_messages]
0707010000006F000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003300000000django-test-migrations-1.5.0/tests/test_exceptions07070100000070000081A40000000000000000000000016802260C0000021E000000000000000000000000000000000000004F00000000django-test-migrations-1.5.0/tests/test_exceptions/test_migration_not_found.pyimport pytest
from django_test_migrations import exceptions
@pytest.mark.parametrize(
('target', 'expected_str'),
[
(('app', None), "Migration ('app', None) not found in migrations plan"),
(
('app', '0047_magic'),
"Migration ('app', '0047_magic') not found in migrations plan",
),
],
)
def test_representation(target, expected_str):
"""Ensure ``MigrationNotInPlan`` has expected string representation."""
assert str(exceptions.MigrationNotInPlan(target)) == expected_str
07070100000071000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002E00000000django-test-migrations-1.5.0/tests/test_logic07070100000072000081A40000000000000000000000016802260C000002EC000000000000000000000000000000000000003F00000000django-test-migrations-1.5.0/tests/test_logic/test_datetime.pyimport datetime
import pytest
from django_test_migrations.logic.datetime import timedelta_to_milliseconds
@pytest.mark.parametrize(
('timedelta', 'expected_result'),
[
(datetime.timedelta(seconds=1), 1000),
(datetime.timedelta(minutes=3), 3 * 60 * 1000),
(datetime.timedelta(hours=2.6), 2.6 * 60 * 60 * 1000),
(
datetime.timedelta(days=4),
4 * 24 * 60 * 60 * 1000,
),
(
datetime.timedelta(minutes=7.4, seconds=47),
7.4 * 60 * 1000 + 47 * 1000,
),
],
)
def test_timedelta_to_milliseconds(timedelta, expected_result):
"""Ensure expected value is returned."""
assert timedelta_to_milliseconds(timedelta) == expected_result
07070100000073000081A40000000000000000000000016802260C000001CC000000000000000000000000000000000000004100000000django-test-migrations-1.5.0/tests/test_logic/test_migrations.pyfrom django_test_migrations.logic.migrations import normalize
def test_normalize_raw_target():
"""Ensure normalize works for ``MigrationTarget``."""
assert normalize(('app', '0074_magic')) == [('app', '0074_magic')]
def test_normalize_list_of_targets():
"""Ensure normalize works for list of ``MigrationTarget``."""
migration_targets = [('app1', None), ('app2', '0001_initial')]
assert normalize(migration_targets) == migration_targets
07070100000074000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000003100000000django-test-migrations-1.5.0/tests/test_migrator07070100000075000081A40000000000000000000000016802260C000003BB000000000000000000000000000000000000004200000000django-test-migrations-1.5.0/tests/test_migrator/test_migrator.pyimport pytest
from django.db.migrations.state import ProjectState
from django_test_migrations.migrator import Migrator
@pytest.mark.django_db
def test_migrator(transactional_db):
"""We only need this test for coverage."""
migrator = Migrator()
old_state = migrator.apply_initial_migration(('main_app', None))
new_state = migrator.apply_tested_migration(('main_app', '0001_initial'))
assert isinstance(old_state, ProjectState)
assert isinstance(new_state, ProjectState)
assert migrator.reset() is None
@pytest.mark.django_db
def test_migrator_list(transactional_db):
"""We only need this test for coverage."""
migrator = Migrator()
old_state = migrator.apply_initial_migration([('main_app', None)])
new_state = migrator.apply_tested_migration([('main_app', '0001_initial')])
assert isinstance(old_state, ProjectState)
assert isinstance(new_state, ProjectState)
assert migrator.reset() is None
07070100000076000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002D00000000django-test-migrations-1.5.0/tests/test_plan07070100000077000081A40000000000000000000000016802260C00000265000000000000000000000000000000000000003900000000django-test-migrations-1.5.0/tests/test_plan/conftest.pyimport pytest
from django.db.migrations import Migration
@pytest.fixture
def plan():
"""Fake migrations plan for testing purposes."""
migrations_plan = [
Migration('0001_initial', 'app1'),
Migration('0002_second', 'app1'),
Migration('0001_initial', 'app2'),
Migration('0003_third', 'app1'),
Migration('0004_fourth', 'app1'),
Migration('0002_second', 'app2'),
Migration('0005_fifth', 'app1'),
Migration('0001_initial', 'app3'),
Migration('0006_sixth', 'app1'),
]
return [(migration, False) for migration in migrations_plan]
07070100000078000081A40000000000000000000000016802260C00000437000000000000000000000000000000000000004400000000django-test-migrations-1.5.0/tests/test_plan/test_all_migrations.pyimport pytest
from django_test_migrations.plan import all_migrations, nodes_to_tuples
@pytest.mark.django_db
def test_all_migrations_main():
"""Testing migrations for a single app only."""
main_migrations = all_migrations('default', ['main_app'])
assert nodes_to_tuples(main_migrations) == [
('main_app', '0001_initial'),
('main_app', '0002_someitem_is_clean'),
('main_app', '0003_update_is_clean'),
('main_app', '0004_auto_20191119_2125'),
('main_app', '0005_auto_20200329_1118'),
]
@pytest.mark.django_db
def test_all_migrations_missing():
"""Testing migrations for a missing app."""
with pytest.raises(LookupError):
all_migrations('default', ['missing_app'])
@pytest.mark.django_db
def test_all_migrations_auth():
"""Testing migrations for a builtin app."""
auth_migrations = all_migrations('default', ['auth'])
assert len(auth_migrations) >= 10
@pytest.mark.django_db
def test_all_migrations_all():
"""Testing migrations for all apps."""
assert len(all_migrations()) >= 17
07070100000079000081A40000000000000000000000016802260C00000573000000000000000000000000000000000000004300000000django-test-migrations-1.5.0/tests/test_plan/test_truncate_plan.pyimport pytest
from django_test_migrations.exceptions import MigrationNotInPlan
from django_test_migrations.plan import truncate_plan
@pytest.mark.parametrize(
('targets', 'index'),
[
([], 9), # full plan for empty targets
([('app1', None)], 0),
([('app1', None), ('app3', None)], 7),
([('app2', '0002_second')], 6),
([('app1', '0002_second'), ('app2', None)], 2),
([('app1', '0003_third'), ('app2', None)], 4),
([('app1', '0003_third'), ('app1', '0005_fifth')], 7),
([('app1', '0003_third'), ('app2', None), ('app3', '0001_initial')], 8),
],
)
def test_truncate_plan(plan, targets, index):
"""Ensure plan is properly truncated for both types migrations names."""
assert truncate_plan(targets, plan) == plan[:index]
def test_empty_plan():
"""Ensure function work when plan is empty."""
assert not truncate_plan([('app1', '0001_initial')], [])
@pytest.mark.parametrize(
'targets',
[
[('app4', None)],
[('app1', '0047_magic')],
[('app1', '0005_fifth'), ('app4', None)],
[('app1', '0005_fifth'), ('app4', '0047_magic'), ('app3', None)],
],
)
def test_migration_target_does_not_exist(plan, targets):
"""Ensure ``MigrationNotInPlan`` is raised when target not in plan."""
with pytest.raises(MigrationNotInPlan):
truncate_plan(targets, plan)
0707010000007A000041ED0000000000000000000000026802260C00000000000000000000000000000000000000000000002C00000000django-test-migrations-1.5.0/tests/test_sql0707010000007B000081A40000000000000000000000016802260C0000072B000000000000000000000000000000000000004600000000django-test-migrations-1.5.0/tests/test_sql/test_drop_models_table.pyfrom django_test_migrations.sql import drop_models_tables
TESTING_DATABASE_NAME = 'test'
def test_drop_models_table_no_tables_detected(mocker):
"""Ensure any ``DROP TABLE`` statement executed when no tables detected."""
testing_connection_mock = mocker.MagicMock()
testing_connection_mock.introspection.django_table_names.return_value = []
connections_mock = mocker.patch('django.db.connections._connections')
connections_mock.test = testing_connection_mock
drop_models_tables(TESTING_DATABASE_NAME)
testing_connection_mock.ops.execute_sql_flush.assert_not_called()
def test_drop_models_table_table_detected(mocker):
"""Ensure ``DROP TABLE`` statements are executed when any table detected."""
testing_connection_mock = mocker.MagicMock()
testing_connection_mock.introspection.django_table_names.return_value = [
'foo_bar',
'foo_baz',
]
connections_mock = mocker.patch('django.db.connections._connections')
connections_mock.test = testing_connection_mock
drop_models_tables(TESTING_DATABASE_NAME)
testing_connection_mock.ops.execute_sql_flush.assert_called_once()
def test_drop_models_table_on_mysql(mocker):
"""Ensure queries disabling/enabling `FOREIGN_KEY_CHECKS` are executed."""
testing_connection_mock = mocker.MagicMock(vendor='mysql')
testing_connection_mock.introspection.django_table_names.return_value = [
'foo_bar',
'foo_baz',
]
connections_mock = mocker.patch('django.db.connections._connections')
connections_mock.test = testing_connection_mock
drop_models_tables(TESTING_DATABASE_NAME)
testing_connection_mock.ops.execute_sql_flush.assert_called_once_with([
'SET FOREIGN_KEY_CHECKS = 0;',
mocker.ANY,
mocker.ANY,
'SET FOREIGN_KEY_CHECKS = 1;',
])
0707010000007C000081A40000000000000000000000016802260C000003ED000000000000000000000000000000000000004000000000django-test-migrations-1.5.0/tests/test_sql/test_flush_utils.pyimport pytest
from django.core.management.color import Style
from django_test_migrations import sql
@pytest.fixture
def testing_connection_mock(mocker):
"""Mock Django connections to check the methods called."""
testing_connection_mock = mocker.MagicMock()
testing_connection_mock.introspection.get_sequences.return_value = []
connections_mock = mocker.patch('django.db.connections._connections')
connections_mock.test = testing_connection_mock
return testing_connection_mock
def test_flush_django_migration_table(mocker, testing_connection_mock):
"""Ensure expected ``connection`` methods are called."""
style = Style()
sql.flush_django_migrations_table('test', style)
testing_connection_mock.ops.sql_flush.assert_called_once_with(
style,
[sql.DJANGO_MIGRATIONS_TABLE_NAME],
reset_sequences=True,
allow_cascade=False,
)
testing_connection_mock.ops.execute_sql_flush.assert_called_once_with(
mocker.ANY,
)
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!342 blocks