Files
django-stubs/mypy_django_plugin/config.py
Petter Friberg edec5a1c99 Notify when Manager.from_queryset happens inside model class body (#824)
* Refactor to more easily support additional config options

* Notify when Manager.from_queryset happens inside model class body

- A warning will be emitted whenever `Manager.from_queryset` happens
  inside of a model class body

* Resolve generated default manager types before final iteration

A default manager on a model should always exist, eventually. Although,
we extend to look through dynamically generated managers on each
iteration instead of deferring until the final iteration.
2022-01-21 19:46:56 +03:00

99 lines
3.3 KiB
Python

import configparser
import textwrap
from functools import partial
from pathlib import Path
from typing import Any, Callable, Dict, NoReturn, Optional
import tomli
INI_USAGE = """
(config)
...
[mypy.plugins.django_stubs]
django_settings_module: str (required)
...
"""
TOML_USAGE = """
(config)
...
[tool.django-stubs]
django_settings_module = str (required)
...
"""
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".format
MISSING_DJANGO_SETTINGS = "missing required 'django_settings_module' config"
INVALID_SETTING = "invalid {key!r}: the setting must be a boolean".format
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",)
django_settings_module: str
def __init__(self, config_file: Optional[str]) -> 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 = tomli.load(f)
except (tomli.TOMLDecodeError, OSError):
toml_exit(COULD_NOT_LOAD_FILE)
try:
config: Dict[str, Any] = data["tool"]["django-stubs"]
except KeyError:
toml_exit(MISSING_SECTION(section="tool.django-stubs"))
if "django_settings_module" not in config:
toml_exit(MISSING_DJANGO_SETTINGS)
self.django_settings_module = config["django_settings_module"]
if not isinstance(self.django_settings_module, str):
toml_exit("invalid 'django_settings_module': the setting must be a string")
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(section=section))
if not parser.has_option(section, "django_settings_module"):
exit_with_error(MISSING_DJANGO_SETTINGS)
self.django_settings_module = parser.get(section, "django_settings_module").strip("'\"")