django-migration-zero 2.0.3__tar.gz → 2.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.ambient-package-update/metadata.py +6 -3
  2. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.github/workflows/ci.yml +10 -9
  3. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.pre-commit-config.yaml +5 -5
  4. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/CHANGES.md +23 -0
  5. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/PKG-INFO +7 -6
  6. django_migration_zero-2.3.2/SECURITY.md +18 -0
  7. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/__init__.py +1 -1
  8. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/helpers/file_system.py +17 -17
  9. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/managers.py +7 -16
  10. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/services/deployment.py +4 -3
  11. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/services/local.py +14 -6
  12. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/conf.py +0 -2
  13. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/features/02_configuration.md +2 -8
  14. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/manage.py +1 -1
  15. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/pyproject.toml +25 -17
  16. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/settings.py +1 -2
  17. django_migration_zero-2.3.2/tests/helpers/test_file_system.py +81 -0
  18. django_migration_zero-2.3.2/tests/management/commands/__init__.py +0 -0
  19. django_migration_zero-2.3.2/tests/services/__init__.py +0 -0
  20. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/services/test_deployment.py +56 -1
  21. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/services/test_local.py +21 -6
  22. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/test_managers.py +2 -1
  23. django_migration_zero-2.0.3/django_migration_zero/settings.py +0 -19
  24. django_migration_zero-2.0.3/tests/helpers/test_file_system.py +0 -49
  25. django_migration_zero-2.0.3/tests/test_settings.py +0 -18
  26. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.ambient-package-update/templates/snippets/content.tpl +0 -0
  27. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.ambient-package-update/templates/snippets/installation.tpl +0 -0
  28. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.ambient-package-update/templates/snippets/tagline.tpl +0 -0
  29. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.coveragerc +0 -0
  30. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.editorconfig +0 -0
  31. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.gitignore +0 -0
  32. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/.readthedocs.yaml +0 -0
  33. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/LICENSE.md +0 -0
  34. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/MANIFEST.in +0 -0
  35. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/README.md +0 -0
  36. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/admin.py +0 -0
  37. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/apps.py +0 -0
  38. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/exceptions.py +0 -0
  39. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/helpers/__init__.py +0 -0
  40. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/helpers/logger.py +0 -0
  41. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/locale/de/LC_MESSAGES/django.mo +0 -0
  42. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/locale/de/LC_MESSAGES/django.po +0 -0
  43. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/management/__init__.py +0 -0
  44. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/management/commands/__init__.py +0 -0
  45. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/management/commands/handle_migration_zero_reset.py +0 -0
  46. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/management/commands/reset_local_migration_files.py +0 -0
  47. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/migrations/0001_initial.py +0 -0
  48. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/migrations/__init__.py +0 -0
  49. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/models.py +0 -0
  50. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/django_migration_zero/services/__init__.py +0 -0
  51. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/Makefile +0 -0
  52. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/features/01_introduction.md +0 -0
  53. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/features/03_workflow.md +0 -0
  54. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/features/04_common_issues_and_solutions.md +0 -0
  55. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/features/05_architecture.md +0 -0
  56. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/features/06_management_commands.md +0 -0
  57. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/features/07_changelog.rst +0 -0
  58. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/index.rst +0 -0
  59. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/docs/make.bat +0 -0
  60. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/scripts/unix/install_requirements.sh +0 -0
  61. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/scripts/unix/publish_to_pypi.sh +0 -0
  62. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/scripts/windows/install_requirements.ps1 +0 -0
  63. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/scripts/windows/publish_to_pypi.ps1 +0 -0
  64. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/setup.cfg +0 -0
  65. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/testapp/__init__.py +0 -0
  66. {django_migration_zero-2.0.3/tests → django_migration_zero-2.3.2/testapp/nested_app}/__init__.py +0 -0
  67. {django_migration_zero-2.0.3/tests/helpers → django_migration_zero-2.3.2/testapp/nested_app/migrations}/__init__.py +0 -0
  68. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/testapp/urls.py +0 -0
  69. {django_migration_zero-2.0.3/tests/management → django_migration_zero-2.3.2/tests}/__init__.py +0 -0
  70. {django_migration_zero-2.0.3/tests/management/commands → django_migration_zero-2.3.2/tests/helpers}/__init__.py +0 -0
  71. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/helpers/test_logger.py +0 -0
  72. {django_migration_zero-2.0.3/tests/services → django_migration_zero-2.3.2/tests/management}/__init__.py +0 -0
  73. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/management/commands/test_handle_migration_zero_reset.py +0 -0
  74. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/management/commands/test_reset_local_migration_files.py +0 -0
  75. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/test_admin.py +0 -0
  76. {django_migration_zero-2.0.3 → django_migration_zero-2.3.2}/tests/test_models.py +0 -0
@@ -8,10 +8,11 @@ from ambient_package_update.metadata.constants import (
8
8
  from ambient_package_update.metadata.maintainer import PackageMaintainer
9
9
  from ambient_package_update.metadata.package import PackageMetadata
10
10
  from ambient_package_update.metadata.readme import ReadmeContent
11
+ from ambient_package_update.metadata.ruff_ignored_inspection import RuffIgnoredInspection
11
12
 
12
13
  METADATA = PackageMetadata(
13
14
  package_name="django_migration_zero",
14
- module_name="django_migration_zero",
15
+ github_package_group="ambient-innovation",
15
16
  authors=[
16
17
  PackageAuthor(
17
18
  name="Ambient Digital",
@@ -26,12 +27,14 @@ METADATA = PackageMetadata(
26
27
  has_migrations=True,
27
28
  readme_content=ReadmeContent(uses_internationalisation=True),
28
29
  dependencies=[
29
- "Django>=3.2",
30
+ f"Django>={SUPPORTED_DJANGO_VERSIONS[0]}",
30
31
  ],
31
32
  supported_django_versions=SUPPORTED_DJANGO_VERSIONS,
32
33
  supported_python_versions=SUPPORTED_PYTHON_VERSIONS,
33
34
  optional_dependencies={
34
35
  "dev": [*DEV_DEPENDENCIES, "unittest-parametrize~=1.3"],
35
36
  },
36
- ruff_ignore_list=[],
37
+ ruff_ignore_list=[
38
+ RuffIgnoredInspection(key="TRY003", comment="Avoid specifying long messages outside the exception class"),
39
+ ],
37
40
  )
@@ -42,14 +42,14 @@ jobs:
42
42
  runs-on: ubuntu-22.04
43
43
  strategy:
44
44
  matrix:
45
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', ]
46
- django-version: ['42', '50', ]
45
+ python-version: ['3.9', '3.10', '3.11', '3.12', ]
46
+ django-version: ['42', '50', '51', ]
47
47
 
48
48
  exclude:
49
- - python-version: '3.8'
50
- django-version: 50
51
49
  - python-version: '3.9'
52
50
  django-version: 50
51
+ - python-version: '3.9'
52
+ django-version: 51
53
53
 
54
54
  steps:
55
55
  - uses: actions/checkout@v4
@@ -64,9 +64,9 @@ jobs:
64
64
  TOXENV: django${{ matrix.django-version }}
65
65
  run: tox
66
66
  - name: Upload coverage data
67
- uses: actions/upload-artifact@v3
67
+ uses: actions/upload-artifact@v4
68
68
  with:
69
- name: coverage-data
69
+ name: coverage-data-${{ matrix.python-version }}-${{ matrix.django-version }}
70
70
  path: '.coverage*'
71
71
 
72
72
  coverage:
@@ -84,11 +84,12 @@ jobs:
84
84
  run: python -m pip install --upgrade coverage[toml]
85
85
 
86
86
  - name: Download data
87
- uses: actions/download-artifact@v3
87
+ uses: actions/download-artifact@v4
88
88
  with:
89
- name: coverage-data
89
+ pattern: coverage-data*
90
+ merge-multiple: true
90
91
 
91
- - name: Combine coverage and fail if it's <100%
92
+ - name: Combine coverage and fail if it's <100.0%
92
93
  run: |
93
94
  python -m coverage html --skip-covered --skip-empty
94
95
  python -m coverage report --fail-under=100.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  repos:
5
5
  - repo: https://github.com/astral-sh/ruff-pre-commit
6
- rev: v0.4.8
6
+ rev: v0.6.4
7
7
  hooks:
8
8
  # Run the Ruff linter.
9
9
  - id: ruff
@@ -12,7 +12,7 @@ repos:
12
12
  - id: ruff-format
13
13
 
14
14
  - repo: https://github.com/adamchainz/blacken-docs
15
- rev: 1.16.0
15
+ rev: 1.18.0
16
16
  hooks:
17
17
  - id: blacken-docs
18
18
  additional_dependencies:
@@ -20,14 +20,14 @@ repos:
20
20
  files: '(?:README\.md|docs\/.*\.(?:md|rst))'
21
21
 
22
22
  - repo: https://github.com/asottile/pyupgrade
23
- rev: v3.16.0
23
+ rev: v3.17.0
24
24
  hooks:
25
25
  - id: pyupgrade
26
- args: [ --py38-plus ]
26
+ args: [ --py39-plus ]
27
27
  stages: [ push ]
28
28
 
29
29
  - repo: https://github.com/adamchainz/django-upgrade
30
- rev: 1.18.0
30
+ rev: 1.21.0
31
31
  hooks:
32
32
  - id: django-upgrade
33
33
  args: [--target-version, "4.2"]
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ * *2.3.2* (2024-09-11)
4
+ * Fixed package name
5
+
6
+ * *2.3.1* (2024-08-12)
7
+ * Fixed test matrix
8
+
9
+ * *2.3.0* (2024-08-12)
10
+ * Added Django 5.1 support
11
+
12
+ * *2.2.0* (2024-07-18)
13
+ * Use ORM to reset `django_migrations` table
14
+ * Lock rows to enable parallel deployments
15
+ * Dropped Python 3.8 support
16
+ * Added multiple ruff linters and updated packages
17
+ * Updated GitHub actions
18
+ * Added SECURITY.md
19
+ * Internal updates via `ambient-package-update`
20
+
21
+ * *2.1.0* (2024-07-09)
22
+ * Discover apps in nested directories
23
+ * Use `BASE_DIR` instead of `MIGRATION_ZERO_APPS_DIR`
24
+ * Fixed bug for migrations with > 4 leading digits
25
+
3
26
  * *2.0.3* (2024-06-21)
4
27
  * Linted docs with `blacken-docs` via `ambient-package-update`
5
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-migration-zero
3
- Version: 2.0.3
3
+ Version: 2.3.2
4
4
  Summary: Holistic implementation of "migration zero" pattern for Django covering local changes and in-production database adjustments.
5
5
  Author-email: Ambient Digital <hello@ambient.digital>
6
6
  Description-Content-Type: text/markdown
@@ -9,32 +9,33 @@ Classifier: Environment :: Web Environment
9
9
  Classifier: Framework :: Django
10
10
  Classifier: Framework :: Django :: 4.2
11
11
  Classifier: Framework :: Django :: 5.0
12
+ Classifier: Framework :: Django :: 5.1
12
13
  Classifier: Intended Audience :: Developers
13
14
  Classifier: License :: OSI Approved :: MIT License
14
15
  Classifier: Natural Language :: English
15
16
  Classifier: Operating System :: OS Independent
16
17
  Classifier: Programming Language :: Python
17
18
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
19
  Classifier: Programming Language :: Python :: 3.9
20
20
  Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Programming Language :: Python :: 3.12
23
23
  Classifier: Topic :: Utilities
24
- Requires-Dist: Django>=3.2
24
+ Requires-Dist: Django>=4.2
25
25
  Requires-Dist: typer~=0.9 ; extra == "dev"
26
26
  Requires-Dist: freezegun~=1.3 ; extra == "dev"
27
27
  Requires-Dist: pytest-django~=4.7 ; extra == "dev"
28
28
  Requires-Dist: pytest-mock~=3.12 ; extra == "dev"
29
29
  Requires-Dist: coverage~=7.3 ; extra == "dev"
30
- Requires-Dist: pre-commit~=3.5 ; extra == "dev"
31
- Requires-Dist: ruff~=0.1.7 ; extra == "dev"
30
+ Requires-Dist: pre-commit~=3.7 ; extra == "dev"
31
+ Requires-Dist: ruff~=0.4 ; extra == "dev"
32
32
  Requires-Dist: sphinx~=7.1 ; extra == "dev"
33
33
  Requires-Dist: sphinx-rtd-theme~=2.0 ; extra == "dev"
34
34
  Requires-Dist: m2r2==0.3.3.post2 ; extra == "dev"
35
35
  Requires-Dist: mistune<2.0.0 ; extra == "dev"
36
36
  Requires-Dist: flit~=3.9 ; extra == "dev"
37
- Requires-Dist: ambient-package-update~=24.6.4 ; extra == "dev"
37
+ Requires-Dist: keyring~=25.2 ; extra == "dev"
38
+ Requires-Dist: ambient-package-update ; extra == "dev"
38
39
  Requires-Dist: unittest-parametrize~=1.3 ; extra == "dev"
39
40
  Project-URL: Bugtracker, https://github.com/ambient-innovation/django-migration-zero/issues
40
41
  Project-URL: Changelog, https://django-migration-zero.readthedocs.io/en/latest/features/changelog.html
@@ -0,0 +1,18 @@
1
+ # Reporting Security Issues
2
+
3
+ The maintainers of this package take security bugs seriously. We appreciate your efforts to responsibly
4
+ disclose your findings, and will make every effort to acknowledge your contributions.
5
+
6
+ To report a security issue, please use the GitHub Security
7
+ Advisory ["Report a Vulnerability"](https://github.com/ambient-innovation/django-migration-zero/security/advisories/new)
8
+ tab.
9
+
10
+ The maintainers will send a response indicating the next steps in handling your report. After the initial reply to
11
+ your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask
12
+ for additional information or guidance.
13
+
14
+ Report security bugs in third-party modules to the person or team maintaining the module.
15
+
16
+ ## Source
17
+
18
+ This file was inspired by: https://github.com/electron/electron/blob/main/SECURITY.md
@@ -3,4 +3,4 @@
3
3
  Holistic implementation of "migration zero" pattern for Django covering local changes and in-production database adjustments.
4
4
  """
5
5
 
6
- __version__ = "2.0.3"
6
+ __version__ = "2.3.2"
@@ -2,62 +2,62 @@ import os
2
2
  import re
3
3
  from os.path import isdir
4
4
  from pathlib import Path
5
- from typing import List
6
5
 
7
6
  from django.apps import apps
7
+ from django.apps.config import AppConfig
8
+ from django.conf import settings
8
9
 
9
10
  from django_migration_zero.helpers.logger import get_logger
10
- from django_migration_zero.settings import get_migration_zero_apps_dir
11
11
 
12
12
  logger = get_logger()
13
13
 
14
14
 
15
- def build_migration_directory_path(*, app_label: str) -> Path:
15
+ def build_migration_directory_path(*, app_path: Path) -> Path:
16
16
  """
17
17
  Get directory to the migration directory of a given local Django app
18
18
  """
19
- return get_migration_zero_apps_dir() / app_label / "migrations"
19
+ return app_path / "migrations"
20
20
 
21
21
 
22
- def get_local_django_apps() -> List[str]:
22
+ def get_local_django_apps() -> list[AppConfig]:
23
23
  """
24
24
  Iterate all installed Django apps and detect local ones.
25
25
  """
26
26
  local_apps = []
27
+ local_path = str(settings.BASE_DIR).replace("\\", "/")
27
28
  logger.info("Getting local Django apps...")
28
29
  for app_config in apps.get_app_configs():
29
- local_path = str(get_migration_zero_apps_dir())
30
- app_path = str(Path(app_config.path))
31
- if local_path in app_path and "site-packages" not in app_path:
30
+ app_path = str(app_config.path).replace("\\", "/")
31
+ if app_path.startswith(local_path) and "site-packages" not in app_path:
32
32
  logger.info(f"Local app {app_config.label!r} discovered.")
33
- local_apps.append(app_config.label)
33
+ local_apps.append(app_config)
34
34
  else:
35
35
  logger.debug(f"App {app_config.label!r} ignored since it's not local.")
36
36
 
37
37
  return local_apps
38
38
 
39
39
 
40
- def has_migration_directory(*, app_label: str) -> bool:
40
+ def has_migration_directory(*, app_path: Path) -> bool:
41
41
  """
42
42
  Determines if the given Django app has a migrations directory and therefore migrations
43
43
  """
44
- possible_migration_dir = build_migration_directory_path(app_label=app_label)
44
+ possible_migration_dir = build_migration_directory_path(app_path=app_path)
45
45
  return True if isdir(possible_migration_dir) else False
46
46
 
47
47
 
48
- def get_migration_files(*, app_label: str, exclude_initials: bool = False) -> List[str]:
48
+ def get_migration_files(*, app_label: str, app_path: Path, exclude_initials: bool = False) -> list[str]:
49
49
  """
50
50
  Returns a list of all migration files detected in the given Django app.
51
51
  """
52
52
  migration_file_list = []
53
53
 
54
54
  logger.info(f"Getting migration files from app {app_label!r}...")
55
- migration_dir = build_migration_directory_path(app_label=app_label)
56
- file_pattern = r"^\d{4}_\w+\.py$"
55
+ migration_dir = build_migration_directory_path(app_path=app_path)
56
+ file_pattern = r"^\d{4,}_\w+\.py$"
57
57
  for filename in os.listdir(migration_dir):
58
58
  if re.match(file_pattern, filename):
59
59
  if exclude_initials:
60
- initial_pattern = r"^\d{4}_initial.py$"
60
+ initial_pattern = r"^\d{4,}_initial.py$"
61
61
  if re.match(initial_pattern, filename):
62
62
  logger.debug(f"File {filename!r} ignored since it's an initial migration.")
63
63
  continue
@@ -70,11 +70,11 @@ def get_migration_files(*, app_label: str, exclude_initials: bool = False) -> Li
70
70
  return migration_file_list
71
71
 
72
72
 
73
- def delete_file(*, filename: str, app_label: str, dry_run: bool = False) -> None:
73
+ def delete_file(*, filename: str, app_path: Path, dry_run: bool = False) -> None:
74
74
  """
75
75
  Physically delete the given file
76
76
  """
77
- file_path = build_migration_directory_path(app_label=app_label) / filename
77
+ file_path = build_migration_directory_path(app_path=app_path) / filename
78
78
  if not dry_run:
79
79
  try:
80
80
  os.unlink(file_path)
@@ -1,35 +1,26 @@
1
+ from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
1
2
  from django.db import ProgrammingError, models
2
3
 
3
4
  from django_migration_zero.exceptions import MissingMigrationZeroConfigRecordError
4
5
  from django_migration_zero.helpers.logger import get_logger
5
6
 
6
7
 
7
- class MigrationZeroConfigurationQuerySet(models.QuerySet):
8
- pass
9
-
10
-
11
8
  class MigrationZeroConfigurationManager(models.Manager):
12
9
  def fetch_singleton(self) -> None:
13
10
  logger = get_logger()
14
11
  try:
15
- number_records = self.count()
12
+ config_singleton = self.select_for_update().get()
16
13
  except ProgrammingError:
17
14
  logger.warning(
18
15
  "The migration zero table is missing. This might be ok for the first installation of "
19
16
  '"django-migration-zero" but if you see this warning after that point, something went sideways.'
20
17
  )
21
- return None
22
-
23
- if number_records > 1:
18
+ config_singleton = None
19
+ except MultipleObjectsReturned as e:
24
20
  raise MissingMigrationZeroConfigRecordError(
25
21
  "Too many configuration records detected. There can only be one."
26
- )
27
-
28
- config_singleton = self.all().first()
29
- if not config_singleton:
30
- raise MissingMigrationZeroConfigRecordError("No configuration record found in the database.")
22
+ ) from e
23
+ except ObjectDoesNotExist as e:
24
+ raise MissingMigrationZeroConfigRecordError("No configuration record found in the database.") from e
31
25
 
32
26
  return config_singleton
33
-
34
-
35
- MigrationZeroConfigurationManager = MigrationZeroConfigurationManager.from_queryset(MigrationZeroConfigurationQuerySet)
@@ -1,7 +1,8 @@
1
1
  from logging import Logger
2
2
 
3
3
  from django.core.management import call_command
4
- from django.db import connections
4
+ from django.db import transaction
5
+ from django.db.migrations.recorder import MigrationRecorder
5
6
 
6
7
  from django_migration_zero.exceptions import InvalidMigrationTreeError
7
8
  from django_migration_zero.helpers.logger import get_logger
@@ -20,6 +21,7 @@ class DatabasePreparationService:
20
21
 
21
22
  self.logger = get_logger()
22
23
 
24
+ @transaction.atomic
23
25
  def process(self):
24
26
  self.logger.info("Starting migration zero database adjustments...")
25
27
 
@@ -34,8 +36,7 @@ class DatabasePreparationService:
34
36
  # records of the other ones
35
37
  self.logger.info("Resetting migration history for all apps...")
36
38
 
37
- with connections["default"].cursor() as cursor:
38
- cursor.execute("DELETE FROM `django_migrations`")
39
+ MigrationRecorder.Migration.objects.all().delete()
39
40
 
40
41
  # Apply migrations via fake because the database is already up-to-date
41
42
  self.logger.info("Populating migration history.")
@@ -1,3 +1,5 @@
1
+ from pathlib import Path
2
+
1
3
  from django.core.management import call_command
2
4
 
3
5
  from django_migration_zero.helpers.file_system import (
@@ -23,17 +25,23 @@ class ResetMigrationFiles:
23
25
 
24
26
  def process(self):
25
27
  logger = get_logger()
26
- local_app_list = get_local_django_apps()
28
+ local_apps = get_local_django_apps()
29
+
30
+ for app_config in local_apps:
31
+ app_path = Path(app_config.path)
27
32
 
28
- for app_label in local_app_list:
29
- if not has_migration_directory(app_label=app_label):
30
- logger.debug(f"Skipping app {app_label!r}. No migration package detected.")
33
+ if not has_migration_directory(app_path=app_path):
34
+ logger.debug(f"Skipping app {app_config.label!r}. No migration package detected.")
31
35
  continue
32
36
 
33
- migration_file_list = get_migration_files(app_label=app_label, exclude_initials=self.exclude_initials)
37
+ migration_file_list = get_migration_files(
38
+ app_label=app_config.label,
39
+ app_path=app_path,
40
+ exclude_initials=self.exclude_initials,
41
+ )
34
42
 
35
43
  for migration_file in migration_file_list:
36
- delete_file(filename=migration_file, app_label=app_label, dry_run=self.dry_run)
44
+ delete_file(filename=migration_file, app_path=app_path, dry_run=self.dry_run)
37
45
 
38
46
  logger.info("Recreating new initial migration files...")
39
47
  call_command("makemigrations")
@@ -67,8 +67,6 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
67
67
 
68
68
  # The theme to use for HTML and HTML Help pages. See the documentation for
69
69
  # a list of builtin themes.
70
- #
71
- # html_theme = "alabaster"
72
70
  html_theme = "sphinx_rtd_theme"
73
71
  html_theme_options = {
74
72
  "display_version": False,
@@ -2,17 +2,11 @@
2
2
 
3
3
  # Settings
4
4
 
5
- This package requires all local Django apps to be inside a single directory. This directory needs to be defined in the
6
- main django settings.
7
-
8
- Usually, you can use the `BASE_DIR` variable from Django's default setup and add a path.
5
+ This package requires all local Django apps to be inside a `BASE_DIR` directory. This directory should be already
6
+ defined in the main django settings.
9
7
 
10
8
  Note that this variable has to be of type `pathlib.Path`.
11
9
 
12
- ```python
13
- MIGRATION_ZERO_APPS_DIR = BASE_DIR / "apps"
14
- ```
15
-
16
10
  ## Logging
17
11
 
18
12
  The scripts are quite chatty. If you want to see what's going on under the hood, just configure a quite default-looking
@@ -7,7 +7,7 @@ import sys
7
7
 
8
8
  def main():
9
9
  """Run administrative tasks."""
10
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_migration_zero.settings")
10
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
11
11
  try:
12
12
  from django.core.management import execute_from_command_line
13
13
  except ImportError as exc:
@@ -14,13 +14,13 @@ classifiers = [
14
14
  "Framework :: Django",
15
15
  "Framework :: Django :: 4.2",
16
16
  "Framework :: Django :: 5.0",
17
+ "Framework :: Django :: 5.1",
17
18
  "Intended Audience :: Developers",
18
19
  "License :: OSI Approved :: MIT License",
19
20
  "Natural Language :: English",
20
21
  "Operating System :: OS Independent",
21
22
  "Programming Language :: Python",
22
23
  "Programming Language :: Python :: 3",
23
- "Programming Language :: Python :: 3.8",
24
24
  "Programming Language :: Python :: 3.9",
25
25
  "Programming Language :: Python :: 3.10",
26
26
  "Programming Language :: Python :: 3.11",
@@ -30,7 +30,7 @@ classifiers = [
30
30
  dynamic = ["version", "description"]
31
31
  license = {"file" = "LICENSE.md"}
32
32
  dependencies = [
33
- 'Django>=3.2',
33
+ 'Django>=4.2',
34
34
  ]
35
35
 
36
36
 
@@ -41,14 +41,15 @@ dev = [
41
41
  'pytest-django~=4.7',
42
42
  'pytest-mock~=3.12',
43
43
  'coverage~=7.3',
44
- 'pre-commit~=3.5',
45
- 'ruff~=0.1.7',
44
+ 'pre-commit~=3.7',
45
+ 'ruff~=0.4',
46
46
  'sphinx~=7.1',
47
47
  'sphinx-rtd-theme~=2.0',
48
48
  'm2r2==0.3.3.post2',
49
49
  'mistune<2.0.0',
50
50
  'flit~=3.9',
51
- 'ambient-package-update~=24.6.4',
51
+ 'keyring~=25.2',
52
+ 'ambient-package-update',
52
53
  'unittest-parametrize~=1.3',
53
54
  ]
54
55
 
@@ -74,18 +75,24 @@ lint.select = [
74
75
  "DTZ", # flake8-datetimez
75
76
  "DJ", # flake8-django
76
77
  "TD", # flake8-to-do
77
- "RUF100", # Removes unnecessary "#noqa" comments
78
+ "RUF", # Ruff-specific rules
78
79
  "YTT", # Avoid non-future-prove usages of "sys"
79
- # "FBT", # Protects you from the "boolean trap bug"
80
80
  "C4", # Checks for unnecessary conversions
81
81
  "PIE", # Bunch of useful rules
82
- # "SIM", # Simplifies your code
83
82
  "INT", # Validates your gettext translation strings
84
83
  "PERF", # PerfLint
85
84
  "PGH", # No all-purpose "# noqa" and eval validation
86
85
  "PL", # PyLint
86
+ "LOG", # flake8-logging
87
+ "TID", # flake8-tidy-imports
88
+ "PLR2004", # Magic numbers
89
+ "BLE", # Checks for except clauses that catch all exceptions
90
+ "ANN401", # Checks that function arguments are annotated with a more specific type than Any
91
+ "TRY", # Clean try/except
92
+ "ERA", # Commented out code
87
93
  ]
88
94
  lint.ignore = [
95
+ 'TRY003', # Avoid specifying long messages outside the exception class
89
96
  ]
90
97
 
91
98
  # Allow autofix for all enabled rules (when `--fix`) is provided.
@@ -100,16 +107,21 @@ lint.fixable = [
100
107
  "DTZ", # flake8-datetimez
101
108
  "DJ", # flake8-django
102
109
  "TD", # flake8-to-do
103
- "RUF100", # Removes unnecessary "#noqa" comments
110
+ "RUF", # Ruff-specific rules
104
111
  "YTT", # Avoid non-future-prove usages of "sys"
105
- # "FBT", # Protects you from the "boolean trap bug"
106
112
  "C4", # Checks for unnecessary conversions
107
113
  "PIE", # Bunch of useful rules
108
- # "SIM", # Simplifies your code
109
114
  "INT", # Validates your gettext translation strings
110
115
  "PERF", # PerfLint
111
116
  "PGH", # No all-purpose "# noqa" and eval validation
112
117
  "PL", # PyLint
118
+ "LOG", # flake8-logging
119
+ "TID", # flake8-tidy-imports
120
+ "PLR2004", # Magic numbers
121
+ "BLE", # Checks for except clauses that catch all exceptions
122
+ "ANN401", # Checks that function arguments are annotated with a more specific type than Any
123
+ "TRY", # Clean try/except
124
+ "ERA", # Commented out code
113
125
  ]
114
126
  lint.unfixable = []
115
127
 
@@ -141,7 +153,7 @@ exclude = [
141
153
  line-length = 120
142
154
 
143
155
  # Allow unused variables when underscore-prefixed.
144
- dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
156
+ lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
145
157
 
146
158
  # Assume Python 3.12
147
159
  target-version = "py312"
@@ -161,22 +173,18 @@ line-ending = "auto"
161
173
 
162
174
  [tool.tox]
163
175
  legacy_tox_ini = """
164
- [tox]
165
- envlist = py{38,39,310,311}-django{32,41,42}
166
- isolated_build = True
167
-
168
176
  [testenv]
169
177
  # Django deprecation overview: https://www.djangoproject.com/download/
170
178
  deps =
171
179
  django42: Django==4.2.*
172
180
  django50: Django==5.0.*
181
+ django51: Django==5.1.*
173
182
  extras = dev,
174
183
  commands =
175
184
  coverage run -m pytest --ds settings tests
176
185
 
177
186
  [gh-actions]
178
187
  python =
179
- 3.8: py38
180
188
  3.9: py39
181
189
  3.10: py310
182
190
  3.11: py311
@@ -42,6 +42,7 @@ INSTALLED_APPS = [
42
42
  "django_migration_zero",
43
43
  # Local
44
44
  "testapp",
45
+ "testapp.nested_app",
45
46
  ]
46
47
 
47
48
  MIDDLEWARE = [
@@ -81,8 +82,6 @@ DATABASES = {
81
82
  }
82
83
  }
83
84
 
84
- MIGRATION_ZERO_APPS_DIR = BASE_DIR / "django-migration-zero"
85
-
86
85
  # Password validation
87
86
  # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
88
87
 
@@ -0,0 +1,81 @@
1
+ from pathlib import Path
2
+ from unittest import mock
3
+
4
+ from django.apps import apps
5
+ from django.test import TestCase
6
+
7
+ from django_migration_zero.helpers.file_system import (
8
+ build_migration_directory_path,
9
+ delete_file,
10
+ get_local_django_apps,
11
+ get_migration_files,
12
+ has_migration_directory,
13
+ )
14
+
15
+
16
+ class HelperFileSystemTest(TestCase):
17
+ def setUp(self):
18
+ self.django_migration_zero_config = apps.get_app_config("django_migration_zero")
19
+ self.testapp_config = apps.get_app_config("testapp")
20
+ self.nested_app_config = apps.get_app_config("nested_app")
21
+
22
+ def test_build_migration_directory_path_regular(self, *args):
23
+ path = Path("/user/workspace/migration_zero/ilyta")
24
+ self.assertEqual(
25
+ build_migration_directory_path(app_path=path), Path("/user/workspace/migration_zero/ilyta/migrations")
26
+ )
27
+
28
+ def test_get_local_django_apps_regular(self):
29
+ self.assertEqual(
30
+ get_local_django_apps(),
31
+ [
32
+ self.django_migration_zero_config,
33
+ self.testapp_config,
34
+ self.nested_app_config,
35
+ ],
36
+ )
37
+
38
+ def test_has_migration_directory_positive_case(self):
39
+ app_path = Path(self.django_migration_zero_config.path)
40
+ self.assertTrue(has_migration_directory(app_path=app_path))
41
+
42
+ def test_has_migration_directory_negative_case(self):
43
+ app_path = Path(self.testapp_config.path)
44
+ self.assertFalse(has_migration_directory(app_path=app_path))
45
+
46
+ def test_has_migration_directory_positive_case_no_migrations(self):
47
+ app_path = Path(self.nested_app_config.path)
48
+ self.assertTrue(has_migration_directory(app_path=app_path))
49
+
50
+ def test_get_migration_files_regular(self):
51
+ self.assertEqual(
52
+ get_migration_files(
53
+ app_label=self.django_migration_zero_config.label,
54
+ app_path=Path(self.django_migration_zero_config.path),
55
+ ),
56
+ ["0001_initial.py"],
57
+ )
58
+
59
+ def test_get_migration_files_exclude_initials(self):
60
+ self.assertEqual(
61
+ get_migration_files(
62
+ app_label=self.django_migration_zero_config.label,
63
+ app_path=Path(self.django_migration_zero_config.path),
64
+ exclude_initials=True,
65
+ ),
66
+ [],
67
+ )
68
+
69
+ @mock.patch("django_migration_zero.helpers.file_system.os.unlink")
70
+ def test_delete_file_regular(self, mocked_unlink):
71
+ delete_file(app_path=Path(self.testapp_config.path), filename="my_file.py")
72
+ mocked_unlink.assert_called_once()
73
+
74
+ @mock.patch("django_migration_zero.helpers.file_system.os.unlink")
75
+ def test_delete_file_dry_run(self, mocked_unlink):
76
+ delete_file(app_path=Path(self.testapp_config.path), filename="my_file.py", dry_run=True)
77
+ mocked_unlink.assert_not_called()
78
+
79
+ @mock.patch("django_migration_zero.helpers.file_system.os.unlink", side_effect=OSError)
80
+ def test_delete_file_os_error(self, *args):
81
+ self.assertIsNone(delete_file(app_path=Path(self.testapp_config.path), filename="my_file.py", dry_run=False))
File without changes
@@ -1,7 +1,9 @@
1
1
  from logging import Logger
2
+ from threading import Thread
2
3
  from unittest import mock
4
+ from unittest.mock import Mock, call
3
5
 
4
- from django.test import TestCase
6
+ from django.test import TestCase, TransactionTestCase
5
7
  from django.utils import timezone
6
8
  from freezegun import freeze_time
7
9
 
@@ -81,3 +83,56 @@ class DatabasePreparationServiceTest(TestCase):
81
83
 
82
84
  self.assertEqual(calls[1].args, ("migrate",))
83
85
  self.assertEqual(calls[1].kwargs, {"check": True})
86
+
87
+
88
+ class DatabasePreparationServiceTestParallelDeployment(TransactionTestCase):
89
+ @mock.patch("django_migration_zero.services.deployment.get_logger")
90
+ def test_process_multiple_threads(self, mock_get_logger):
91
+ """Test parallel deployments to multiple pods."""
92
+ # Setup
93
+ mock_logger_info = Mock(return_value=None)
94
+ mock_get_logger.return_value = Mock(info=mock_logger_info)
95
+ config, _ = MigrationZeroConfiguration.objects.update_or_create(
96
+ defaults={
97
+ "migration_imminent": True,
98
+ "migration_date": timezone.now().date(),
99
+ }
100
+ )
101
+
102
+ # Testable
103
+ number_of_pods = 1
104
+ threads = [Thread(target=DatabasePreparationService().process) for _ in range(number_of_pods)]
105
+
106
+ for thread in threads:
107
+ thread.start()
108
+
109
+ for thread in threads:
110
+ thread.join()
111
+
112
+ # Assertions
113
+ self.assertEqual(mock_logger_info.call_count, 6 + number_of_pods, msg=mock_logger_info.call_args_list)
114
+ mock_logger_info.assert_has_calls(
115
+ [
116
+ call("Starting migration zero database adjustments..."),
117
+ call("Resetting migration history for all apps..."),
118
+ call("Populating migration history."),
119
+ call("Checking migration integrity."),
120
+ call("All good."),
121
+ call("Deactivating migration zero switch in database."),
122
+ call("Process successfully finished."),
123
+ ],
124
+ any_order=True,
125
+ )
126
+ self.assertEqual(
127
+ len(
128
+ [
129
+ mock_call
130
+ for mock_call in mock_logger_info.call_args_list
131
+ if mock_call == call("Starting migration zero database adjustments...")
132
+ ]
133
+ ),
134
+ number_of_pods,
135
+ )
136
+
137
+ config.refresh_from_db()
138
+ self.assertFalse(config.migration_imminent)
@@ -1,5 +1,7 @@
1
+ from pathlib import Path
1
2
  from unittest import mock
2
3
 
4
+ from django.apps import apps
3
5
  from django.test import TestCase
4
6
 
5
7
  from django_migration_zero.services.local import ResetMigrationFiles
@@ -7,26 +9,39 @@ from django_migration_zero.services.local import ResetMigrationFiles
7
9
 
8
10
  @mock.patch("django_migration_zero.services.local.delete_file")
9
11
  class ResetMigrationFilesTest(TestCase):
12
+ def setUp(self):
13
+ self.django_migration_zero_config = apps.get_app_config("django_migration_zero")
14
+ self.testapp_config = apps.get_app_config("testapp")
15
+ self.nested_app_config = apps.get_app_config("nested_app")
16
+
10
17
  def test_init_regular(self, *args):
11
18
  service = ResetMigrationFiles(dry_run=True, exclude_initials=True)
12
19
 
13
20
  self.assertTrue(service.dry_run)
14
21
  self.assertTrue(service.exclude_initials)
15
22
 
16
- @mock.patch("django_migration_zero.services.local.get_local_django_apps", return_value=["django_migration_zero"])
17
23
  @mock.patch("django_migration_zero.services.local.has_migration_directory", return_value=True)
18
24
  def test_process_case_app_with_migration_found(self, mocked_has_migration_directory, *args):
19
25
  service = ResetMigrationFiles()
20
- service.process()
26
+
27
+ with mock.patch(
28
+ "django_migration_zero.services.local.get_local_django_apps",
29
+ return_value=[self.django_migration_zero_config],
30
+ ):
31
+ service.process()
21
32
 
22
33
  # Assertion
23
34
  mocked_has_migration_directory.assert_called()
24
35
 
25
- @mock.patch("django_migration_zero.services.local.get_local_django_apps", return_value=["django_migration_zero"])
26
36
  @mock.patch("django_migration_zero.services.local.has_migration_directory", return_value=False)
27
37
  def test_process_case_app_with_no_migration_found(self, mocked_has_migration_directory, *args):
28
38
  service = ResetMigrationFiles()
29
- service.process()
39
+
40
+ with mock.patch(
41
+ "django_migration_zero.services.local.get_local_django_apps",
42
+ return_value=[self.testapp_config, self.nested_app_config],
43
+ ):
44
+ service.process()
30
45
 
31
46
  # Assertion
32
47
  mocked_has_migration_directory.assert_called()
@@ -37,7 +52,7 @@ class ResetMigrationFilesTest(TestCase):
37
52
 
38
53
  # Assertion
39
54
  mocked_delete_file.assert_called_with(
40
- filename="0001_initial.py", app_label="django_migration_zero", dry_run=False
55
+ filename="0001_initial.py", app_path=Path(self.django_migration_zero_config.path), dry_run=False
41
56
  )
42
57
 
43
58
  def test_process_delete_file_case_dry_run(self, mocked_delete_file):
@@ -46,7 +61,7 @@ class ResetMigrationFilesTest(TestCase):
46
61
 
47
62
  # Assertion
48
63
  mocked_delete_file.assert_called_with(
49
- filename="0001_initial.py", app_label="django_migration_zero", dry_run=True
64
+ filename="0001_initial.py", app_path=Path(self.django_migration_zero_config.path), dry_run=True
50
65
  )
51
66
 
52
67
  def test_process_delete_file_case_exclude_initials(self, mocked_delete_file):
@@ -5,6 +5,7 @@ from django.db import ProgrammingError
5
5
  from django.test import TestCase
6
6
 
7
7
  from django_migration_zero.exceptions import MissingMigrationZeroConfigRecordError
8
+ from django_migration_zero.managers import MigrationZeroConfigurationManager
8
9
  from django_migration_zero.models import MigrationZeroConfiguration
9
10
 
10
11
 
@@ -18,7 +19,7 @@ class MigrationZeroConfigurationManagerTest(TestCase):
18
19
  def test_fetch_singleton_singleton_exists_via_migration(self):
19
20
  self.assertEqual(MigrationZeroConfiguration.objects.all().count(), 1)
20
21
 
21
- @mock.patch.object(MigrationZeroConfiguration.objects, "count", side_effect=ProgrammingError)
22
+ @mock.patch.object(MigrationZeroConfigurationManager, "select_for_update", side_effect=ProgrammingError)
22
23
  def test_fetch_singleton_database_error(self, *args):
23
24
  self.assertIsNone(MigrationZeroConfiguration.objects.fetch_singleton())
24
25
 
@@ -1,19 +0,0 @@
1
- from pathlib import Path
2
-
3
- from django.conf import settings
4
-
5
- from django_migration_zero.exceptions import InvalidMigrationAppsDirPathError
6
-
7
-
8
- def get_migration_zero_apps_dir() -> Path:
9
- """
10
- Helper method to get the settings variable and validate the type
11
- """
12
- migration_zero_apps_dir = getattr(settings, "MIGRATION_ZERO_APPS_DIR", Path("/"))
13
-
14
- if not isinstance(migration_zero_apps_dir, Path):
15
- raise InvalidMigrationAppsDirPathError(
16
- 'Settings variable "MIGRATION_ZERO_APPS_DIR" has to be of type pathlib.Path.'
17
- )
18
-
19
- return migration_zero_apps_dir
@@ -1,49 +0,0 @@
1
- from pathlib import Path
2
- from unittest import mock
3
-
4
- from django.test import TestCase, override_settings
5
-
6
- from django_migration_zero.helpers.file_system import (
7
- build_migration_directory_path,
8
- delete_file,
9
- get_local_django_apps,
10
- get_migration_files,
11
- has_migration_directory,
12
- )
13
-
14
-
15
- class HelperFileSystemTest(TestCase):
16
- @override_settings(MIGRATION_ZERO_APPS_DIR=Path("/user/workspace/migration_zero/"))
17
- def test_build_migration_directory_path_regular(self, *args):
18
- self.assertEqual(
19
- build_migration_directory_path(app_label="ilyta"), Path("/user/workspace/migration_zero/ilyta/migrations")
20
- )
21
-
22
- def test_get_local_django_apps_regular(self):
23
- self.assertEqual(get_local_django_apps(), ["django_migration_zero", "testapp"])
24
-
25
- def test_has_migration_directory_positive_case(self):
26
- self.assertTrue(has_migration_directory(app_label="django_migration_zero"))
27
-
28
- def test_has_migration_directory_negative_case(self):
29
- self.assertFalse(has_migration_directory(app_label="testapp"))
30
-
31
- def test_get_migration_files_regular(self):
32
- self.assertEqual(get_migration_files(app_label="django_migration_zero"), ["0001_initial.py"])
33
-
34
- def test_get_migration_files_exclude_initials(self):
35
- self.assertEqual(get_migration_files(app_label="django_migration_zero", exclude_initials=True), [])
36
-
37
- @mock.patch("django_migration_zero.helpers.file_system.os.unlink")
38
- def test_delete_file_regular(self, mocked_unlink):
39
- delete_file(app_label="testapp", filename="my_file.py")
40
- mocked_unlink.assert_called_once()
41
-
42
- @mock.patch("django_migration_zero.helpers.file_system.os.unlink")
43
- def test_delete_file_dry_run(self, mocked_unlink):
44
- delete_file(app_label="testapp", filename="my_file.py", dry_run=True)
45
- mocked_unlink.assert_not_called()
46
-
47
- @mock.patch("django_migration_zero.helpers.file_system.os.unlink", side_effect=OSError)
48
- def test_delete_file_os_error(self, *args):
49
- self.assertIsNone(delete_file(app_label="testapp", filename="my_file.py", dry_run=False))
@@ -1,18 +0,0 @@
1
- from django.conf import settings
2
- from django.test import TestCase, override_settings
3
-
4
- from django_migration_zero.exceptions import InvalidMigrationAppsDirPathError
5
- from django_migration_zero.settings import get_migration_zero_apps_dir
6
-
7
-
8
- class SettingsTest(TestCase):
9
- def test_get_migration_zero_apps_dir_regular(self):
10
- self.assertEqual(get_migration_zero_apps_dir(), settings.MIGRATION_ZERO_APPS_DIR)
11
-
12
- @override_settings(MIGRATION_ZERO_APPS_DIR="/usr/path/to/apps")
13
- def test_get_migration_zero_apps_dir_wrong_type(self):
14
- with self.assertRaisesMessage(
15
- InvalidMigrationAppsDirPathError,
16
- 'Settings variable "MIGRATION_ZERO_APPS_DIR" has to be of type pathlib.Path.',
17
- ):
18
- get_migration_zero_apps_dir()