Skip to content

Commit e4efd15

Browse files
committed
Add support for configure with pyproject.toml
Support for TOML configuration files with sections like [tool.mypy] and [tool.mypy-mypackage.*]. Signed-off-by: Pedro Lacerda <[email protected]>
1 parent 6a956d7 commit e4efd15

File tree

8 files changed

+78
-49
lines changed

8 files changed

+78
-49
lines changed

mypy/defaults.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
CONFIG_FILE = 'mypy.ini'
66
SHARED_CONFIG_FILES = ('setup.cfg',)
77
USER_CONFIG_FILES = ('~/.mypy.ini',)
8-
CONFIG_FILES = (CONFIG_FILE,) + SHARED_CONFIG_FILES + USER_CONFIG_FILES
8+
PYPROJECT_CONFIG_FILES = ('pyproject.toml',)
9+
CONFIG_FILES = (CONFIG_FILE,) + SHARED_CONFIG_FILES + USER_CONFIG_FILES + PYPROJECT_CONFIG_FILES

mypy/main.py

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import sys
1010
import time
1111

12-
from typing import Any, Dict, List, Mapping, Optional, Tuple, Callable
12+
from typing import Any, Dict, List, Mapping, Optional, Tuple, Callable, Type
13+
14+
import toml # type: ignore
1315

1416
from mypy import build
1517
from mypy import defaults
@@ -892,35 +894,54 @@ def parse_config_file(options: Options, filename: Optional[str]) -> None:
892894
else:
893895
config_files = tuple(map(os.path.expanduser, defaults.CONFIG_FILES))
894896

895-
parser = configparser.RawConfigParser()
896-
897897
for config_file in config_files:
898898
if not os.path.exists(config_file):
899899
continue
900-
try:
901-
parser.read(config_file)
902-
except configparser.Error as err:
903-
print("%s: %s" % (config_file, err), file=sys.stderr)
900+
if config_file.endswith('.toml'):
901+
try:
902+
config = toml.load(config_file)
903+
except Exception as err:
904+
print("%s: %s" % (config_file, err), file=sys.stderr)
905+
else:
906+
section_title = 'tool.mypy'
907+
file_read = config_file
908+
909+
plain_config = {}
910+
for name, section in config.get('tool', {}).items():
911+
if name == 'mypy' or name.startswith('mypy-'):
912+
plain_config['tool.%s' % name] = section
913+
config = plain_config
914+
break
904915
else:
905-
file_read = config_file
906-
options.config_file = file_read
907-
break
916+
try:
917+
config = configparser.RawConfigParser()
918+
config.read(config_file)
919+
except configparser.Error as err:
920+
print("%s: %s" % (config_file, err), file=sys.stderr)
921+
else:
922+
file_read = config_file
923+
options.config_file = file_read
924+
section_title = 'mypy'
925+
break
908926
else:
909927
return
910928

911-
if 'mypy' not in parser:
929+
config # type: Dict[str, Any]
930+
931+
if section_title not in config:
912932
if filename or file_read not in defaults.SHARED_CONFIG_FILES:
913-
print("%s: No [mypy] section in config file" % file_read, file=sys.stderr)
933+
print("%s: No [%s] section in config file" % (file_read, section_title),
934+
file=sys.stderr)
914935
else:
915-
section = parser['mypy']
916-
prefix = '%s: [%s]' % (file_read, 'mypy')
936+
section = config[section_title]
937+
prefix = '%s: [%s]' % (file_read, section_title)
917938
updates, report_dirs = parse_section(prefix, options, section)
918939
for k, v in updates.items():
919940
setattr(options, k, v)
920941
options.report_dirs.update(report_dirs)
921942

922-
for name, section in parser.items():
923-
if name.startswith('mypy-'):
943+
for name, section in config.items():
944+
if name.startswith(section_title + '-'):
924945
prefix = '%s: [%s]' % (file_read, name)
925946
updates, report_dirs = parse_section(prefix, options, section)
926947
if report_dirs:
@@ -932,7 +953,7 @@ def parse_config_file(options: Options, filename: Optional[str]) -> None:
932953
(prefix, ', '.join(sorted(set(updates) - Options.PER_MODULE_OPTIONS))),
933954
file=sys.stderr)
934955
updates = {k: v for k, v in updates.items() if k in Options.PER_MODULE_OPTIONS}
935-
globs = name[5:]
956+
globs = name[len(section_title) + 1:]
936957
for glob in globs.split(','):
937958
# For backwards compatibility, replace (back)slashes with dots.
938959
glob = glob.replace(os.sep, '.')
@@ -984,18 +1005,24 @@ def parse_section(prefix: str, template: Options,
9841005
ct = type(dv)
9851006
v = None # type: Any
9861007
try:
987-
if ct is bool:
988-
v = section.getboolean(key) # type: ignore # Until better stub
989-
elif callable(ct):
990-
try:
991-
v = ct(section.get(key))
992-
except argparse.ArgumentTypeError as err:
993-
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
1008+
if isinstance(section, configparser.SectionProxy):
1009+
if ct is bool:
1010+
v = section.getboolean(key) # type: ignore # Until better stub
1011+
elif callable(ct):
1012+
try:
1013+
v = ct(section.get(key))
1014+
except argparse.ArgumentTypeError as err: # XXX ct raises ArgumentTypeError?
1015+
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
1016+
continue
1017+
else:
1018+
print("%s: Don't know what type %s should have" % (prefix, key),
1019+
file=sys.stderr)
9941020
continue
9951021
else:
996-
print("%s: Don't know what type %s should have" % (prefix, key), file=sys.stderr)
997-
continue
998-
except ValueError as err:
1022+
v = section[key]
1023+
if not isinstance(v, ct): # type: ignore # No idea
1024+
raise ValueError(f"{v} is not an instance of {ct}")
1025+
except (ValueError, KeyError) as err:
9991026
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
10001027
continue
10011028
if key == 'silent_imports':

mypy/test/testfinegrained.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def get_options(self,
172172
options.follow_imports = 'error'
173173

174174
for name, _ in testcase.files:
175-
if 'mypy.ini' in name:
175+
if os.path.basename(name) in ['mypy.ini', 'pyproject.toml']:
176176
parse_config_file(options, name)
177177
break
178178

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def run(self):
104104
classifiers=classifiers,
105105
cmdclass={'build_py': CustomPythonBuild},
106106
install_requires = ['typed-ast >= 1.1.0, < 1.2.0',
107+
'toml >= 0.7.0, < 0.8.0'
107108
],
108109
extras_require = {
109110
':python_version < "3.5"': 'typing >= 3.5.3',

test-data/unit/check-custom-plugin.test

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,28 @@ reveal_type(f()) # E: Revealed type is 'builtins.int'
1212
plugins=<ROOT>/test-data/unit/plugins/fnplugin.py
1313

1414
[case testFunctionPluginFullnameIsNotNone]
15-
# flags: --config-file tmp/mypy.ini
15+
# flags: --config-file tmp/mypy.toml
1616
from typing import Callable, TypeVar
1717
f: Callable[[], None]
1818
T = TypeVar('T')
1919
def g(x: T) -> T: return x # This strips out the name of a callable
2020
g(f)()
21-
[file mypy.ini]
22-
[[mypy]
23-
plugins=<ROOT>/test-data/unit/plugins/fnplugin.py
21+
[file mypy.toml]
22+
[[tool.mypy]
23+
plugins=<ROOT>/test-data/unit/plugins/fnplugin.py"
2424

2525
[case testTwoPlugins]
26-
# flags: --config-file tmp/mypy.ini
26+
# flags: --config-file tmp/mypy.toml
2727
def f(): ...
2828
def g(): ...
2929
def h(): ...
3030
reveal_type(f()) # E: Revealed type is 'builtins.int'
3131
reveal_type(g()) # E: Revealed type is 'builtins.str'
3232
reveal_type(h()) # E: Revealed type is 'Any'
33-
[file mypy.ini]
34-
[[mypy]
35-
plugins=<ROOT>/test-data/unit/plugins/fnplugin.py,
36-
<ROOT>/test-data/unit/plugins/plugin2.py
33+
[file mypy.toml]
34+
[[tool.mypy]
35+
plugins=["<ROOT>/test-data/unit/plugins/fnplugin.py",
36+
"<ROOT>/test-data/unit/plugins/plugin2.py"]
3737

3838
[case testMissingPlugin]
3939
# flags: --config-file tmp/mypy.ini

test-data/unit/check-flags.test

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ if z: # E: Condition must be a boolean
472472

473473

474474
[case testPerFileStrictOptionalBasic]
475-
# flags: --config-file tmp/mypy.ini
475+
# flags: --config-file tmp/pyproject.toml
476476
import standard, optional
477477

478478
[file standard.py]
@@ -482,11 +482,11 @@ x = None
482482
x = 0
483483
x = None # E: Incompatible types in assignment (expression has type "None", variable has type "int")
484484

485-
[file mypy.ini]
486-
[[mypy]
487-
strict_optional = False
488-
[[mypy-optional]
489-
strict_optional = True
485+
[file pyproject.toml]
486+
[[tool.mypy]
487+
strict_optional = false
488+
[[tool.mypy-optional]
489+
strict_optional = true
490490

491491

492492
[case testPerFileStrictOptionalBasicImportStandard]

test-data/unit/cmdline.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,6 @@ import d
12571257
[case testCacheMap]
12581258
-- This just checks that a valid --cache-map triple is accepted.
12591259
-- (Errors are too verbose to check.)
1260-
# cmd: mypy a.py --cache-map a.py a.meta.json a.data.json
1260+
# cmd: mypy a.py --cache-map a.py a.meta.json a.data.json
12611261
[file a.py]
12621262
[out]

test-data/unit/fine-grained.test

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7141,10 +7141,10 @@ def f() -> str: return '0'
71417141
==
71427142

71437143
[case testRefreshIgnoreErrors1]
7144-
[file mypy.ini]
7145-
[[mypy]
7146-
[[mypy-b]
7147-
ignore_errors = True
7144+
[file pyproject.toml]
7145+
[[tool.mypy]
7146+
[[tool.mypy-b]
7147+
ignore_errors = true
71487148
[file a.py]
71497149
y = '1'
71507150
[file a.py.2]

0 commit comments

Comments
 (0)