Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/235/support makemmigration customisation #266

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 4 additions & 69 deletions django_migration_linter/management/commands/lintmigrations.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
from __future__ import annotations

import configparser
import itertools
import os
import sys
from importlib import import_module
from typing import Any, Callable

import toml
from django.conf import settings
from django.core.management.base import BaseCommand, CommandParser

from ...constants import __version__
Expand All @@ -17,14 +12,7 @@
configure_logging,
extract_warnings_as_errors_option,
register_linting_configuration_options,
)

CONFIG_NAME = "django_migration_linter"
PYPROJECT_TOML = "pyproject.toml"
DEFAULT_CONFIG_FILES = (
f".{CONFIG_NAME}.cfg",
"setup.cfg",
"tox.ini",
set_defaults_from_conf,
)


Expand Down Expand Up @@ -133,26 +121,17 @@ def add_arguments(self, parser: CommandParser) -> None:
help="don't print linting messages to stdout",
)
register_linting_configuration_options(parser)
set_defaults_from_conf(parser)

def handle(self, *args, **options):
django_settings_options = self.read_django_settings(options)
config_options = self.read_config_file(options)
toml_options = self.read_toml_file(options)
for k, v in itertools.chain(
django_settings_options.items(),
config_options.items(),
toml_options.items(),
):
if not options[k]:
options[k] = v

configure_logging(options["verbosity"])

(
warnings_as_errors_tests,
all_warnings_as_errors,
) = extract_warnings_as_errors_option(options["warnings_as_errors"])

configure_logging(options["verbosity"])

root_path = options["project_root_path"] or os.path.dirname(
import_module(os.getenv("DJANGO_SETTINGS_MODULE")).__file__
)
Expand Down Expand Up @@ -186,49 +165,5 @@ def handle(self, *args, **options):
if linter.has_errors:
sys.exit(1)

@staticmethod
def read_django_settings(options: dict[str, Any]) -> dict[str, Any]:
django_settings_options = dict()

django_migration_linter_settings = getattr(
settings, "MIGRATION_LINTER_OPTIONS", dict()
)
for key in options:
if key in django_migration_linter_settings:
django_settings_options[key] = django_migration_linter_settings[key]

return django_settings_options

@staticmethod
def read_config_file(options: dict[str, Any]) -> dict[str, Any]:
config_options = dict()

config_parser = configparser.ConfigParser()
config_parser.read(DEFAULT_CONFIG_FILES, encoding="utf-8")
for key, value in options.items():
config_get_fn: Callable
if isinstance(value, bool):
config_get_fn = config_parser.getboolean
else:
config_get_fn = config_parser.get

config_value = config_get_fn(CONFIG_NAME, key, fallback=None)
if config_value is not None:
config_options[key] = config_value
return config_options

@staticmethod
def read_toml_file(options: dict[str, Any]) -> dict[str, Any]:
toml_options = dict()

if os.path.exists(PYPROJECT_TOML):
pyproject_toml = toml.load(PYPROJECT_TOML)
section = pyproject_toml.get("tool", {}).get(CONFIG_NAME, {})
for key in options:
if key in section:
toml_options[key] = section[key]

return toml_options

def get_version(self) -> str:
return __version__
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
configure_logging,
extract_warnings_as_errors_option,
register_linting_configuration_options,
set_defaults_from_conf,
)


Expand Down Expand Up @@ -45,14 +46,17 @@ def add_arguments(self, parser: CommandParser) -> None:
help="Lint newly generated migrations.",
)
register_linting_configuration_options(parser)
set_defaults_from_conf(parser)


def handle(self, *app_labels, **options):
configure_logging(options["verbosity"])

self.lint = options["lint"]
self.database = options["database"]
self.exclude_migrations_tests = options["exclude_migration_tests"]
self.warnings_as_errors = options["warnings_as_errors"]
self.sql_analyser = options["sql_analyser"]
configure_logging(options["verbosity"])
return super().handle(*app_labels, **options)

def write_migration_files(self, changes: dict[str, list[Migration]]) -> None:
Expand Down
99 changes: 99 additions & 0 deletions django_migration_linter/management/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
from __future__ import annotations

from typing import Any, Callable
import configparser
import logging
import itertools
import os

import toml

from django.core.management import CommandParser
from django.conf import settings

from ..sql_analyser.analyser import ANALYSER_STRING_MAPPING


CONFIG_NAME = "django_migration_linter"
PYPROJECT_TOML = "pyproject.toml"
DEFAULT_CONFIG_FILES = (
f".{CONFIG_NAME}.cfg",
"setup.cfg",
"tox.ini",
)


def register_linting_configuration_options(parser: CommandParser) -> None:
parser.add_argument(
"--database",
Expand Down Expand Up @@ -65,3 +81,86 @@ def extract_warnings_as_errors_option(
all_warnings_as_errors = False

return warnings_as_errors_tests, all_warnings_as_errors

def set_defaults_from_conf(parser) -> None:
"""
Load options defined in config.

Settings are loaded in priority order, where higher
priority settings override lower.

1. Django Settings
2. Config files
3. pyproject.toml

Any command line options then override config file args.
"""

# Parse args to get the relevant options for the current command
args = parser.parse_args()
options = vars(args)

# Retrieve relevant config values
toml_options = read_toml_file(options)
config_options = read_config_file(options)
django_settings_options = read_django_settings(options)

# Merge values from configs in priority order toml -> conf -> settings
conf_options = {k:v for k,v in itertools.chain(toml_options.items(),config_options.items(),django_settings_options.items())}

# Override default argparse arguments with conf options
parser.set_defaults(**conf_options)


def read_django_settings(options: dict[str, Any]) -> dict[str, Any]:
"""
Read configuration from django settings
"""
django_settings_options = dict()

django_migration_linter_settings = getattr(
settings, "MIGRATION_LINTER_OPTIONS", dict()
)
for key in options:
if key in django_migration_linter_settings:
django_settings_options[key] = django_migration_linter_settings[key]

return django_migration_linter_settings


def read_config_file(options: dict[str, Any]) -> dict[str, Any]:
"""
Read config options from any of the supported files specified
in DEFAULT_CONFIG_FILES.
"""
config_options = dict()

config_parser = configparser.ConfigParser()
config_parser.read(DEFAULT_CONFIG_FILES, encoding="utf-8")
for key, value in options.items():
config_get_fn: Callable
if isinstance(value, bool):
config_get_fn = config_parser.getboolean
else:
config_get_fn = config_parser.get

config_value = config_get_fn(CONFIG_NAME, key, fallback=None)
if config_value is not None:
config_options[key] = config_value
return config_options


def read_toml_file(options: dict[str, Any]) -> dict[str, Any]:
"""
Read config options from toml file.
"""
toml_options = dict()

if os.path.exists(PYPROJECT_TOML):
pyproject_toml = toml.load(PYPROJECT_TOML)
section = pyproject_toml.get("tool", {}).get(CONFIG_NAME, {})
for key in options:
if key in section:
toml_options[key] = section[key]

return toml_options
4 changes: 4 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ The migration test codes can be found in the [corresponding source code files](.
[3YOURMIND](https://www.3yourmind.com/) is running the linter on every build getting pushed through CI.
That enables to be sure that the migrations will allow A/B testing, Blue/Green deployment, and they won't break your development environment.
A non-zero error code is returned to express that at least one invalid migration has been found.


## Postgis
You might be able to get away with using the `postgresql` sql analyser option with PostGIS. This can be configured using the `sql_analyser` option.
3 changes: 3 additions & 0 deletions tests/unit/files/test_config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[tool.django_migration_linter]
sql_analyser = "postgresql"
not_a_real_setting = true
3 changes: 3 additions & 0 deletions tests/unit/files/test_setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[django_migration_linter]
traceback = True
not_a_real_setting = true
Loading