Skip to content

Commit 8a64d87

Browse files
authored
Output a more clear configuration error (#421)
* Output a more clear configuration error * Performance related improvements in config read handling * Check python 3.6 with travis * Revert the .travis.yml py36 inclusion * Added tests for input error handling * Thanks isort, isorted it out * Make exit() function a bit more aesthetic * Single quote -> double quote docstrings * Whitespace removed
1 parent 60f3f9d commit 8a64d87

File tree

3 files changed

+109
-20
lines changed

3 files changed

+109
-20
lines changed

mypy_django_plugin/main.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import configparser
22
from functools import partial
3-
from typing import Callable, Dict, List, Optional, Tuple
3+
from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
44

55
from django.db.models.fields.related import RelatedField
6-
from mypy.errors import Errors
76
from mypy.nodes import MypyFile, TypeInfo
87
from mypy.options import Options
98
from mypy.plugin import (
@@ -52,25 +51,40 @@ def add_new_manager_base(ctx: ClassDefContext) -> None:
5251

5352

5453
def extract_django_settings_module(config_file_path: Optional[str]) -> str:
55-
errors = Errors()
56-
if config_file_path is None:
57-
errors.report(0, None, "'django_settings_module' is not set: no mypy config file specified")
58-
errors.raise_error()
54+
55+
def exit(error_type: int) -> NoReturn:
56+
"""Using mypy's argument parser, raise `SystemExit` to fail hard if validation fails.
57+
58+
Considering that the plugin's startup duration is around double as long as mypy's, this aims to
59+
import and construct objects only when that's required - which happens once and terminates the
60+
run. Considering that most of the runs are successful, there's no need for this to linger in the
61+
global scope.
62+
"""
63+
from mypy.main import CapturableArgumentParser
64+
65+
usage = """(config)
66+
...
67+
[mypy.plugins.django_stubs]
68+
django_settings_module: str (required)
69+
...
70+
""".replace("\n" + 8 * " ", "\n")
71+
handler = CapturableArgumentParser(prog='(django-stubs) mypy', usage=usage)
72+
messages = {1: 'mypy config file is not specified or found',
73+
2: 'no section [mypy.plugins.django-stubs]',
74+
3: 'the setting is not provided'}
75+
handler.error("'django_settings_module' is not set: " + messages[error_type])
5976

6077
parser = configparser.ConfigParser()
61-
parser.read(config_file_path) # type: ignore
62-
63-
if not parser.has_section('mypy.plugins.django-stubs'):
64-
errors.report(0, None, "'django_settings_module' is not set: no section [mypy.plugins.django-stubs]",
65-
file=config_file_path)
66-
errors.raise_error()
67-
if not parser.has_option('mypy.plugins.django-stubs', 'django_settings_module'):
68-
errors.report(0, None, "'django_settings_module' is not set: setting is not provided",
69-
file=config_file_path)
70-
errors.raise_error()
71-
72-
django_settings_module = parser.get('mypy.plugins.django-stubs', 'django_settings_module').strip('\'"')
73-
return django_settings_module
78+
try:
79+
parser.read_file(open(cast(str, config_file_path), 'r'), source=config_file_path)
80+
except (IsADirectoryError, OSError):
81+
exit(1)
82+
83+
section = 'mypy.plugins.django-stubs'
84+
if not parser.has_section(section):
85+
exit(2)
86+
settings = parser.get(section, 'django_settings_module', fallback=None) or exit(3)
87+
return cast(str, settings).strip('\'"')
7488

7589

7690
class NewSemanalDjangoPlugin(Plugin):

pytest.ini

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[pytest]
2-
testpaths = ./test-data
2+
testpaths =
3+
./test-plugin
4+
./test-data
35
addopts =
46
--tb=native
57
-s

test-plugin/test_error_handling.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import tempfile
2+
import typing
3+
4+
import pytest
5+
6+
from mypy_django_plugin.main import extract_django_settings_module
7+
8+
TEMPLATE = """usage: (config)
9+
...
10+
[mypy.plugins.django_stubs]
11+
django_settings_module: str (required)
12+
...
13+
(django-stubs) mypy: error: 'django_settings_module' is not set: {}
14+
"""
15+
16+
17+
@pytest.mark.parametrize(
18+
'config_file_contents,message_part',
19+
[
20+
pytest.param(
21+
None,
22+
'mypy config file is not specified or found',
23+
id='missing-file',
24+
),
25+
pytest.param(
26+
['[not-really-django-stubs]'],
27+
'no section [mypy.plugins.django-stubs]',
28+
id='missing-section',
29+
),
30+
pytest.param(
31+
['[mypy.plugins.django-stubs]',
32+
'\tnot_django_not_settings_module = badbadmodule'],
33+
'the setting is not provided',
34+
id='missing-settings-module',
35+
),
36+
pytest.param(
37+
['[mypy.plugins.django-stubs]'],
38+
'the setting is not provided',
39+
id='no-settings-given',
40+
),
41+
],
42+
)
43+
def test_misconfiguration_handling(capsys, config_file_contents, message_part):
44+
# type: (typing.Any, typing.List[str], str) -> None
45+
"""Invalid configuration raises `SystemExit` with a precise error message."""
46+
with tempfile.NamedTemporaryFile(mode='w+') as config_file:
47+
if not config_file_contents:
48+
config_file.close()
49+
else:
50+
config_file.write('\n'.join(config_file_contents).expandtabs(4))
51+
config_file.seek(0)
52+
53+
with pytest.raises(SystemExit, match='2'):
54+
extract_django_settings_module(config_file.name)
55+
56+
error_message = TEMPLATE.format(message_part)
57+
assert error_message == capsys.readouterr().err
58+
59+
60+
def test_correct_configuration() -> None:
61+
"""Django settings module gets extracted given valid configuration."""
62+
config_file_contents = [
63+
'[mypy.plugins.django-stubs]',
64+
'\tsome_other_setting = setting',
65+
'\tdjango_settings_module = my.module',
66+
]
67+
with tempfile.NamedTemporaryFile(mode='w+') as config_file:
68+
config_file.write('\n'.join(config_file_contents).expandtabs(4))
69+
config_file.seek(0)
70+
71+
extracted = extract_django_settings_module(config_file.name)
72+
73+
assert extracted == 'my.module'

0 commit comments

Comments
 (0)