mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-13 15:31:55 +08:00
Adds support for pyproject.toml files (#639)
* Adds support for pyproject.toml files Since mypy 0.900 the pyproject.toml files are supported. This PR adds a support for it. It searchs for a `tool.django-stubs` section. This is an example configuration: ``` [tool.django-stubs] django_settings_module = "config.settings.local" ``` Fixes #638 * Added TOML tests * Use textwrap.dedent instead of trying to manually replace spaces
This commit is contained in:
12
README.md
12
README.md
@@ -30,7 +30,17 @@ django_settings_module = "myproject.settings"
|
|||||||
|
|
||||||
in your `mypy.ini` or `setup.cfg` [file](https://mypy.readthedocs.io/en/latest/config_file.html).
|
in your `mypy.ini` or `setup.cfg` [file](https://mypy.readthedocs.io/en/latest/config_file.html).
|
||||||
|
|
||||||
Two things happeining here:
|
[pyproject.toml](https://mypy.readthedocs.io/en/stable/config_file.html#using-a-pyproject-toml-file) configurations are also supported:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.mypy]
|
||||||
|
plugins = ["mypy_django_plugin.main"]
|
||||||
|
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = "myproject.settings"
|
||||||
|
```
|
||||||
|
|
||||||
|
Two things happening here:
|
||||||
|
|
||||||
1. We need to explicitly list our plugin to be loaded by `mypy`
|
1. We need to explicitly list our plugin to be loaded by `mypy`
|
||||||
2. Our plugin also requires `django` settings module (what you put into `DJANGO_SETTINGS_MODULE` variable) to be specified
|
2. Our plugin also requires `django` settings module (what you put into `DJANGO_SETTINGS_MODULE` variable) to be specified
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ pre-commit==2.7.1
|
|||||||
pytest==6.1.1
|
pytest==6.1.1
|
||||||
pytest-mypy-plugins==1.6.1
|
pytest-mypy-plugins==1.6.1
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
|
types-toml==0.1.1
|
||||||
-e ./django_stubs_ext
|
-e ./django_stubs_ext
|
||||||
-e .
|
-e .
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ class IncompleteDefnException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def is_toml(filename: str) -> bool:
|
||||||
|
return filename.lower().endswith(".toml")
|
||||||
|
|
||||||
|
|
||||||
def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]:
|
def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]:
|
||||||
if "." not in fullname:
|
if "." not in fullname:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import configparser
|
import configparser
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
|
from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
|
||||||
|
|
||||||
|
import toml
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
from mypy.modulefinder import mypy_path
|
from mypy.modulefinder import mypy_path
|
||||||
from mypy.nodes import MypyFile, TypeInfo
|
from mypy.nodes import MypyFile, TypeInfo
|
||||||
@@ -63,15 +65,14 @@ def extract_django_settings_module(config_file_path: Optional[str]) -> str:
|
|||||||
"""
|
"""
|
||||||
from mypy.main import CapturableArgumentParser
|
from mypy.main import CapturableArgumentParser
|
||||||
|
|
||||||
usage = """(config)
|
usage = """
|
||||||
|
(config)
|
||||||
...
|
...
|
||||||
[mypy.plugins.django_stubs]
|
[mypy.plugins.django_stubs]
|
||||||
django_settings_module: str (required)
|
django_settings_module: str (required)
|
||||||
...
|
...
|
||||||
""".replace(
|
"""
|
||||||
"\n" + 8 * " ", "\n"
|
handler = CapturableArgumentParser(prog="(django-stubs) mypy", usage=textwrap.dedent(usage))
|
||||||
)
|
|
||||||
handler = CapturableArgumentParser(prog="(django-stubs) mypy", usage=usage)
|
|
||||||
messages = {
|
messages = {
|
||||||
1: "mypy config file is not specified or found",
|
1: "mypy config file is not specified or found",
|
||||||
2: "no section [mypy.plugins.django-stubs]",
|
2: "no section [mypy.plugins.django-stubs]",
|
||||||
@@ -79,18 +80,53 @@ def extract_django_settings_module(config_file_path: Optional[str]) -> str:
|
|||||||
}
|
}
|
||||||
handler.error("'django_settings_module' is not set: " + messages[error_type])
|
handler.error("'django_settings_module' is not set: " + messages[error_type])
|
||||||
|
|
||||||
parser = configparser.ConfigParser()
|
def exit_toml(error_type: int) -> NoReturn:
|
||||||
try:
|
from mypy.main import CapturableArgumentParser
|
||||||
with open(cast(str, config_file_path)) as handle:
|
|
||||||
parser.read_file(handle, source=config_file_path)
|
|
||||||
except (IsADirectoryError, OSError):
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
section = "mypy.plugins.django-stubs"
|
usage = """
|
||||||
if not parser.has_section(section):
|
(config)
|
||||||
exit(2)
|
...
|
||||||
settings = parser.get(section, "django_settings_module", fallback=None) or exit(3)
|
[tool.django-stubs]
|
||||||
return cast(str, settings).strip("'\"")
|
django_settings_module = str (required)
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
handler = CapturableArgumentParser(prog="(django-stubs) mypy", usage=textwrap.dedent(usage))
|
||||||
|
messages = {
|
||||||
|
1: "mypy config file is not specified or found",
|
||||||
|
2: "no section [tool.django-stubs]",
|
||||||
|
3: "the setting is not provided",
|
||||||
|
4: "the setting must be a string",
|
||||||
|
}
|
||||||
|
handler.error("'django_settings_module' not found or invalid: " + messages[error_type])
|
||||||
|
|
||||||
|
if config_file_path and helpers.is_toml(config_file_path):
|
||||||
|
toml_data = toml.load(config_file_path)
|
||||||
|
try:
|
||||||
|
config = toml_data["tool"]["django-stubs"]
|
||||||
|
except KeyError:
|
||||||
|
exit_toml(2)
|
||||||
|
|
||||||
|
if "django_settings_module" not in config:
|
||||||
|
exit_toml(3)
|
||||||
|
|
||||||
|
if not isinstance(config["django_settings_module"], str):
|
||||||
|
exit_toml(4)
|
||||||
|
|
||||||
|
return config["django_settings_module"]
|
||||||
|
else:
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
with open(cast(str, config_file_path)) as handle:
|
||||||
|
parser.read_file(handle, source=config_file_path)
|
||||||
|
except (IsADirectoryError, OSError):
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
section = "mypy.plugins.django-stubs"
|
||||||
|
if not parser.has_section(section):
|
||||||
|
exit(2)
|
||||||
|
settings = parser.get(section, "django_settings_module", fallback=None) or exit(3)
|
||||||
|
|
||||||
|
return cast(str, settings).strip("'\"")
|
||||||
|
|
||||||
|
|
||||||
class NewSemanalDjangoPlugin(Plugin):
|
class NewSemanalDjangoPlugin(Plugin):
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -26,6 +26,7 @@ dependencies = [
|
|||||||
"django",
|
"django",
|
||||||
"django-stubs-ext",
|
"django-stubs-ext",
|
||||||
"types-pytz",
|
"types-pytz",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import pytest
|
|||||||
|
|
||||||
from mypy_django_plugin.main import extract_django_settings_module
|
from mypy_django_plugin.main import extract_django_settings_module
|
||||||
|
|
||||||
TEMPLATE = """usage: (config)
|
TEMPLATE = """
|
||||||
|
(config)
|
||||||
...
|
...
|
||||||
[mypy.plugins.django_stubs]
|
[mypy.plugins.django_stubs]
|
||||||
django_settings_module: str (required)
|
django_settings_module: str (required)
|
||||||
@@ -13,6 +14,15 @@ TEMPLATE = """usage: (config)
|
|||||||
(django-stubs) mypy: error: 'django_settings_module' is not set: {}
|
(django-stubs) mypy: error: 'django_settings_module' is not set: {}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
TEMPLATE_TOML = """
|
||||||
|
(config)
|
||||||
|
...
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = str (required)
|
||||||
|
...
|
||||||
|
(django-stubs) mypy: error: 'django_settings_module' not found or invalid: {}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"config_file_contents,message_part",
|
"config_file_contents,message_part",
|
||||||
@@ -52,10 +62,64 @@ def test_misconfiguration_handling(capsys, config_file_contents, message_part):
|
|||||||
with pytest.raises(SystemExit, match="2"):
|
with pytest.raises(SystemExit, match="2"):
|
||||||
extract_django_settings_module(config_file.name)
|
extract_django_settings_module(config_file.name)
|
||||||
|
|
||||||
error_message = TEMPLATE.format(message_part)
|
error_message = "usage: " + TEMPLATE.format(message_part)
|
||||||
assert error_message == capsys.readouterr().err
|
assert error_message == capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"config_file_contents,message_part",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = 123
|
||||||
|
""",
|
||||||
|
"the setting must be a string",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
[tool.not-really-django-stubs]
|
||||||
|
django_settings_module = "my.module"
|
||||||
|
""",
|
||||||
|
"no section [tool.django-stubs]",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
[tool.django-stubs]
|
||||||
|
not_django_not_settings_module = "badbadmodule"
|
||||||
|
""",
|
||||||
|
"the setting is not provided",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_toml_misconfiguration_handling(capsys, config_file_contents, message_part):
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w+", suffix=".toml") as config_file:
|
||||||
|
config_file.write(config_file_contents)
|
||||||
|
config_file.seek(0)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="2"):
|
||||||
|
extract_django_settings_module(config_file.name)
|
||||||
|
|
||||||
|
error_message = "usage: " + TEMPLATE_TOML.format(message_part)
|
||||||
|
assert error_message == capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_correct_toml_configuration() -> None:
|
||||||
|
config_file_contents = """
|
||||||
|
[tool.django-stubs]
|
||||||
|
some_other_setting = "setting"
|
||||||
|
django_settings_module = "my.module"
|
||||||
|
"""
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w+", suffix=".toml") as config_file:
|
||||||
|
config_file.write(config_file_contents)
|
||||||
|
config_file.seek(0)
|
||||||
|
|
||||||
|
extracted = extract_django_settings_module(config_file.name)
|
||||||
|
|
||||||
|
assert extracted == "my.module"
|
||||||
|
|
||||||
|
|
||||||
def test_correct_configuration() -> None:
|
def test_correct_configuration() -> None:
|
||||||
"""Django settings module gets extracted given valid configuration."""
|
"""Django settings module gets extracted given valid configuration."""
|
||||||
config_file_contents = [
|
config_file_contents = [
|
||||||
|
|||||||
Reference in New Issue
Block a user