File: config.py

package info (click to toggle)
python-django-stubs 5.2.9-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,832 kB
  • sloc: python: 5,185; makefile: 15; sh: 8
file content (149 lines) | stat: -rw-r--r-- 5,628 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import configparser
import os
import sys
import textwrap
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, NoReturn

if TYPE_CHECKING:
    from collections.abc import Callable

if sys.version_info[:2] >= (3, 11):
    import tomllib
else:
    import tomli as tomllib

INI_USAGE = """
(config)
...
[mypy.plugins.django-stubs]
django_settings_module = str (default: `os.getenv("DJANGO_SETTINGS_MODULE")`)
strict_settings = bool (default: true)
strict_model_abstract_attrs = bool (default: true)
...
"""
TOML_USAGE = """
(config)
...
[tool.django-stubs]
django_settings_module = str (default: `os.getenv("DJANGO_SETTINGS_MODULE")`)
strict_settings = bool (default: true)
strict_model_abstract_attrs = bool (default: true)
...
"""
INVALID_FILE = "mypy config file is not specified or found"
COULD_NOT_LOAD_FILE = "could not load configuration file"
MISSING_SECTION = "no section [{section}] found"
DJANGO_SETTINGS_ENV_VAR = "DJANGO_SETTINGS_MODULE"
MISSING_DJANGO_SETTINGS = (
    "missing required 'django_settings_module' config.\n"
    f"Either specify this config or set your `{DJANGO_SETTINGS_ENV_VAR}` env var"
)
INVALID_BOOL_SETTING = "invalid {key!r}: the setting must be a boolean"


def exit_with_error(msg: str, is_toml: bool = False) -> NoReturn:
    """Using mypy's argument parser, raise `SystemExit` to fail hard if validation fails.

    Considering that the plugin's startup duration is around double as long as mypy's, this aims to
    import and construct objects only when that's required - which happens once and terminates the
    run. Considering that most of the runs are successful, there's no need for this to linger in the
    global scope.
    """
    from mypy.main import CapturableArgumentParser

    handler = CapturableArgumentParser(
        prog="(django-stubs) mypy", usage=textwrap.dedent(TOML_USAGE if is_toml else INI_USAGE)
    )
    handler.error(msg)


class DjangoPluginConfig:
    __slots__ = ("django_settings_module", "strict_model_abstract_attrs", "strict_settings")

    django_settings_module: str
    strict_settings: bool

    def __init__(self, config_file: str | None) -> None:
        if not config_file:
            exit_with_error(INVALID_FILE)

        filepath = Path(config_file)
        if not filepath.is_file():
            exit_with_error(INVALID_FILE)

        if filepath.suffix.lower() == ".toml":
            self.parse_toml_file(filepath)
        else:
            self.parse_ini_file(filepath)

    def parse_toml_file(self, filepath: Path) -> None:
        toml_exit: Callable[[str], NoReturn] = partial(exit_with_error, is_toml=True)
        try:
            with filepath.open(mode="rb") as f:
                data = tomllib.load(f)
        except (tomllib.TOMLDecodeError, OSError):
            toml_exit(COULD_NOT_LOAD_FILE)

        try:
            config: dict[str, Any] = data["tool"]["django-stubs"]
        except KeyError:
            toml_exit(MISSING_SECTION.format(section="tool.django-stubs"))

        django_settings_module = config.get("django_settings_module") or os.getenv(DJANGO_SETTINGS_ENV_VAR)
        if not django_settings_module:
            toml_exit(MISSING_DJANGO_SETTINGS)

        self.django_settings_module = django_settings_module

        if not isinstance(self.django_settings_module, str):
            toml_exit("invalid 'django_settings_module': the setting must be a string")

        self.strict_settings = config.get("strict_settings", True)
        if not isinstance(self.strict_settings, bool):
            toml_exit(INVALID_BOOL_SETTING.format(key="strict_settings"))
        self.strict_model_abstract_attrs = config.get("strict_model_abstract_attrs", True)
        if not isinstance(self.strict_model_abstract_attrs, bool):
            toml_exit(INVALID_BOOL_SETTING.format(key="strict_model_abstract_attrs"))

    def parse_ini_file(self, filepath: Path) -> None:
        parser = configparser.ConfigParser()
        try:
            with filepath.open(encoding="utf-8") as f:
                parser.read_file(f, source=str(filepath))
        except OSError:
            exit_with_error(COULD_NOT_LOAD_FILE)

        section = "mypy.plugins.django-stubs"
        if not parser.has_section(section):
            exit_with_error(MISSING_SECTION.format(section=section))

        if parser.has_option(section, "django_settings_module"):
            django_settings_module = parser.get(section, "django_settings_module").strip("'\"")
        else:
            django_settings_module = os.getenv(DJANGO_SETTINGS_ENV_VAR, "")

        if not django_settings_module:
            exit_with_error(MISSING_DJANGO_SETTINGS)

        self.django_settings_module = django_settings_module

        try:
            self.strict_settings = parser.getboolean(section, "strict_settings", fallback=True)
        except ValueError:
            exit_with_error(INVALID_BOOL_SETTING.format(key="strict_settings"))

        try:
            self.strict_model_abstract_attrs = parser.getboolean(section, "strict_model_abstract_attrs", fallback=True)
        except ValueError:
            exit_with_error(INVALID_BOOL_SETTING.format(key="strict_model_abstract_attrs"))

    def to_json(self, extra_data: dict[str, Any]) -> dict[str, Any]:
        """We use this method to reset mypy cache via `report_config_data` hook."""
        return {
            "django_settings_module": self.django_settings_module,
            "strict_settings": self.strict_settings,
            "strict_model_abstract_attrs": self.strict_model_abstract_attrs,
            **dict(sorted(extra_data.items())),
        }