Skip to content

Commit 02b64bc

Browse files
committed
Advancing support for pyproject.toml
1 parent ac3e5ce commit 02b64bc

File tree

8 files changed

+132
-33
lines changed

8 files changed

+132
-33
lines changed

mypy/config_parser.py

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
import sys
88

9+
import toml # type: ignore
910
from typing import Any, Dict, List, Mapping, Optional, Tuple, TextIO
1011
from typing_extensions import Final
1112

@@ -48,11 +49,22 @@ def split_and_match_files(paths: str) -> List[str]:
4849
4950
Where a path/glob matches no file, we still include the raw path in the resulting list.
5051
52+
Returns a list of file paths
53+
"""
54+
55+
return match_files(paths.split(','))
56+
57+
58+
def match_files(paths: List[str]) -> List[str]:
59+
"""Take list of files/directories (with support for globbing through the glob library).
60+
61+
Where a path/glob matches no file, we still include the raw path in the resulting list.
62+
5163
Returns a list of file paths
5264
"""
5365
expanded_paths = []
5466

55-
for path in paths.split(','):
67+
for path in paths:
5668
path = expand_path(path.strip())
5769
globbed_files = fileglob.glob(path, recursive=True)
5870
if globbed_files:
@@ -67,7 +79,7 @@ def split_and_match_files(paths: str) -> List[str]:
6779
# sufficient, and we don't have to do anything here. This table
6880
# exists to specify types for values initialized to None or container
6981
# types.
70-
config_types = {
82+
ini_config_types = {
7183
'python_version': parse_version,
7284
'strict_optional_whitelist': lambda s: s.split(),
7385
'custom_typing_module': str,
@@ -88,6 +100,16 @@ def split_and_match_files(paths: str) -> List[str]:
88100
} # type: Final
89101

90102

103+
toml_type_conversors = {
104+
'python_version': parse_version,
105+
'custom_typeshed_dir': expand_path,
106+
'mypy_path': lambda l: [expand_path(p) for p in l],
107+
'files': match_files,
108+
'cache_dir': expand_path,
109+
'python_executable': expand_path,
110+
}
111+
112+
91113
def parse_config_file(options: Options, filename: Optional[str],
92114
stdout: Optional[TextIO] = None,
93115
stderr: Optional[TextIO] = None) -> None:
@@ -97,13 +119,90 @@ def parse_config_file(options: Options, filename: Optional[str],
97119
98120
If filename is None, fall back to default config files.
99121
"""
122+
if filename is None or os.path.splitext(filename)[1] == '.toml':
123+
parse_toml_config_file(options, filename, stderr)
124+
else:
125+
parse_ini_config_file(options, filename, stdout, stderr)
126+
127+
128+
def parse_toml_config_file(options: Options, filename: Optional[str],
129+
stdout: Optional[TextIO] = None,
130+
stderr: Optional[TextIO] = None) -> None:
131+
stderr = stderr or sys.stderr
132+
133+
# Load the toml config file,
134+
config_file = filename or defaults.TOML_CONFIG_FILE
135+
if not os.path.exists(config_file):
136+
return
137+
try:
138+
table = toml.load(config_file) # type: Dict[str, Any]
139+
except (TypeError, toml.TomlDecodeError, IOError) as err:
140+
print("%s: %s" % (config_file, err), file=stderr)
141+
return
142+
else:
143+
options.config_file = config_file
144+
145+
if 'mypy' not in table:
146+
print("%s: No [mypy] table in config file" % config_file, file=stderr)
147+
return
148+
149+
# Handle the mypy table.
150+
for key, value in table['mypy'].items():
151+
152+
# Is an option.
153+
if not isinstance(value, dict):
154+
155+
# Is a report directory.
156+
if key.endswith('_report'):
157+
report_type = key[:-7].replace('_', '-')
158+
if report_type in defaults.REPORTER_NAMES:
159+
options.report_dirs[report_type] = table['mypy'][key]
160+
else:
161+
print("%s: Unrecognized report type: %s" %
162+
(config_file, key),
163+
file=stderr)
164+
else:
165+
if key in toml_type_conversors:
166+
value = toml_type_conversors[key](value)
167+
setattr(options, key, value)
168+
169+
# Is a subtable. Should be a package/module glob.
170+
else:
171+
glob = key
172+
if (any(c in glob for c in '?[]!') or
173+
any('*' in x and x != '*' for x in glob.split('.'))):
174+
print("%s: Patterns must be fully-qualified module names, optionally "
175+
"with '*' in some components (e.g spam.*.eggs.*)"
176+
% config_file, file=stderr)
177+
178+
values = {}
179+
for subkey, value in table['mypy'][key].items():
180+
if subkey.endswith('_report'):
181+
print("Per-module tables [%s] should not specify reports (%s)" %
182+
(key, subkey), file=stderr)
183+
continue
184+
elif subkey not in PER_MODULE_OPTIONS:
185+
print("Per-module tables [%s] should only specify per-module flags (%s)" %
186+
(key, subkey), file=stderr)
187+
continue
188+
189+
if subkey in toml_type_conversors:
190+
value = toml_type_conversors[subkey](value)
191+
values[subkey] = value
192+
193+
options.per_module_options[glob] = values
194+
195+
196+
def parse_ini_config_file(options: Options, filename: Optional[str],
197+
stdout: Optional[TextIO] = None,
198+
stderr: Optional[TextIO] = None) -> None:
100199
stdout = stdout or sys.stdout
101200
stderr = stderr or sys.stderr
102201

103202
if filename is not None:
104203
config_files = (filename,) # type: Tuple[str, ...]
105204
else:
106-
config_files = tuple(map(os.path.expanduser, defaults.CONFIG_FILES))
205+
config_files = tuple(map(os.path.expanduser, defaults.INI_CONFIG_FILES))
107206

108207
parser = configparser.RawConfigParser()
109208

@@ -175,8 +274,8 @@ def parse_section(prefix: str, template: Options,
175274
for key in section:
176275
invert = False
177276
options_key = key
178-
if key in config_types:
179-
ct = config_types[key]
277+
if key in ini_config_types:
278+
ct = ini_config_types[key]
180279
else:
181280
dv = None
182281
# We have to keep new_semantic_analyzer in Options

mypy/defaults.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
PYTHON3_VERSION = (3, 6) # type: Final
77
PYTHON3_VERSION_MIN = (3, 4) # type: Final
88
CACHE_DIR = '.mypy_cache' # type: Final
9-
CONFIG_FILE = 'mypy.ini' # type: Final
9+
INI_CONFIG_FILE = 'mypy.ini' # type: Final
1010
SHARED_CONFIG_FILES = ['setup.cfg', ] # type: Final
1111
USER_CONFIG_FILES = ['~/.config/mypy/config', '~/.mypy.ini', ] # type: Final
1212
if os.environ.get('XDG_CONFIG_HOME'):
1313
USER_CONFIG_FILES.insert(0, os.path.join(os.environ['XDG_CONFIG_HOME'], 'mypy/config'))
14-
15-
CONFIG_FILES = [CONFIG_FILE, ] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final
14+
TOML_CONFIG_FILE = 'pyproject.toml'
15+
INI_CONFIG_FILES = [INI_CONFIG_FILE, ] + SHARED_CONFIG_FILES + USER_CONFIG_FILES
1616

1717
# This must include all reporters defined in mypy.report. This is defined here
1818
# to make reporter names available without importing mypy.report -- this speeds

mypy/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ def add_invertible_flag(flag: str,
437437
config_group.add_argument(
438438
'--config-file',
439439
help="Configuration file, must have a [mypy] section "
440-
"(defaults to {})".format(', '.join(defaults.CONFIG_FILES)))
440+
"(defaults to {})".format(', '.join(defaults.INI_CONFIG_FILES)))
441441
add_invertible_flag('--warn-unused-configs', default=False, strict_flag=True,
442442
help="Warn about unused '[mypy-<pattern>]' config sections",
443443
group=config_group)

mypy/test/testfinegrained.py

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

194194
for name, _ in testcase.files:
195-
if 'mypy.ini' in name:
195+
if os.path.basename(name) in ['mypy.ini', 'pyproject.toml']:
196196
parse_config_file(options, name)
197197
break
198198

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def run(self):
190190
install_requires=['typed_ast >= 1.4.0, < 1.5.0',
191191
'typing_extensions>=3.7.4',
192192
'mypy_extensions >= 0.4.3, < 0.5.0',
193+
'toml >= 0.10.0, < 0.11.0',
193194
],
194195
# Same here.
195196
extras_require={'dmypy': 'psutil >= 4.0'},

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,28 @@ reveal_type(f()) # N: Revealed type is 'builtins.int'
2020
plugins=fnplugin
2121

2222
[case testFunctionPluginFullnameIsNotNone]
23-
# flags: --config-file tmp/mypy.ini
23+
# flags: --config-file tmp/mypy.toml
2424
from typing import Callable, TypeVar
2525
f: Callable[[], None]
2626
T = TypeVar('T')
2727
def g(x: T) -> T: return x # This strips out the name of a callable
2828
g(f)()
29-
[file mypy.ini]
30-
\[mypy]
31-
plugins=<ROOT>/test-data/unit/plugins/fnplugin.py
29+
[file mypy.toml]
30+
\[tool.mypy]
31+
plugins="<ROOT>/test-data/unit/plugins/fnplugin.py"
3232

3333
[case testTwoPlugins]
34-
# flags: --config-file tmp/mypy.ini
34+
# flags: --config-file tmp/mypy.toml
3535
def f(): ...
3636
def g(): ...
3737
def h(): ...
38-
reveal_type(f()) # N: Revealed type is 'builtins.int'
39-
reveal_type(g()) # N: Revealed type is 'builtins.str'
40-
reveal_type(h()) # N: Revealed type is 'Any'
41-
[file mypy.ini]
42-
\[mypy]
43-
plugins=<ROOT>/test-data/unit/plugins/fnplugin.py,
44-
<ROOT>/test-data/unit/plugins/plugin2.py
38+
reveal_type(f()) # E: Revealed type is 'builtins.int'
39+
reveal_type(g()) # E: Revealed type is 'builtins.str'
40+
reveal_type(h()) # E: Revealed type is 'Any'
41+
[file mypy.toml]
42+
\[tool.mypy]
43+
plugins=["<ROOT>/test-data/unit/plugins/fnplugin.py",
44+
"<ROOT>/test-data/unit/plugins/plugin2.py"]
4545

4646
[case testTwoPluginsMixedType]
4747
# flags: --config-file tmp/mypy.ini

test-data/unit/check-flags.test

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ disallow_incomplete_defs = True
479479

480480

481481
[case testPerFileStrictOptionalBasic]
482-
# flags: --config-file tmp/mypy.ini
482+
# flags: --config-file tmp/pyproject.toml
483483
import standard, optional
484484

485485
[file standard.py]
@@ -491,12 +491,11 @@ x = 0
491491
if int():
492492
x = None # E: Incompatible types in assignment (expression has type "None", variable has type "int")
493493

494-
[file mypy.ini]
495-
\[mypy]
496-
strict_optional = False
497-
\[mypy-optional]
498-
strict_optional = True
499-
494+
[file pyproject.toml]
495+
\[tool.mypy]
496+
strict_optional = false
497+
\[tool.mypy-optional]
498+
strict_optional = true
500499

501500
[case testPerFileStrictOptionalBasicImportStandard]
502501
# flags: --config-file tmp/mypy.ini

test-data/unit/fine-grained.test

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

77707770
[case testRefreshIgnoreErrors1]
7771-
[file mypy.ini]
7772-
\[mypy]
7773-
\[mypy-b]
7774-
ignore_errors = True
7771+
[file pyproject.toml]
7772+
\[tool.mypy]
7773+
\[tool.mypy-b]
7774+
ignore_errors = true
77757775
[file a.py]
77767776
y = '1'
77777777
[file a.py.2]

0 commit comments

Comments
 (0)