diff --git a/.gitignore b/.gitignore index c1113eab637a..c6238dc32beb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ mypyc/doc/_build *.iml /out/ .venv*/ +venv/ .mypy_cache/ .incremental_checker_cache.json .cache @@ -44,6 +45,8 @@ htmlcov bin/ lib/ include/ +.python-version +pyvenv.cfg .tox pip-wheel-metadata diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 27b370407d39..a5fa34a9d959 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -97,7 +97,7 @@ Config file This flag makes mypy read configuration settings from the given file. - By default settings are read from ``mypy.ini``, ``.mypy.ini``, or ``setup.cfg`` + By default settings are read from ``mypy.ini``, ``.mypy.ini``, ``pyproject.toml``, or ``setup.cfg`` in the current directory. Settings override mypy's built-in defaults and command line flags can override settings. diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 1c3607033afc..c707d2b162a5 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -4,8 +4,8 @@ The mypy configuration file =========================== Mypy supports reading configuration settings from a file. By default -it uses the file ``mypy.ini`` with a fallback to ``.mypy.ini``, then ``setup.cfg`` in -the current directory, then ``$XDG_CONFIG_HOME/mypy/config``, then +it uses the file ``mypy.ini`` with a fallback to ``.mypy.ini``, then ``pyproject.toml``, +then ``setup.cfg`` in the current directory, then ``$XDG_CONFIG_HOME/mypy/config``, then ``~/.config/mypy/config``, and finally ``.mypy.ini`` in the user home directory if none of them are found; the :option:`--config-file ` command-line flag can be used to read a different file instead (see :ref:`config-file-flag`). @@ -885,5 +885,84 @@ These options may only be set in the global section (``[mypy]``). Controls how much debug output will be generated. Higher numbers are more verbose. + +Using a pyproject.toml file +*************************** + +Instead of using a ``mypy.ini`` file, a ``pyproject.toml`` file (as specified by +`PEP 518`_) may be used instead. A few notes on doing so: + +* The ``[mypy]`` section should have ``tool.`` prepended to its name: + + * I.e., ``[mypy]`` would become ``[tool.mypy]`` + +* The module specific sections should be moved into ``[[tool.mypy.overrides]]`` sections: + + * For example, ``[mypy-packagename]`` would become: + +.. code-block:: toml + + [[tool.mypy.overrides]] + module = 'packagename' + ... + +* Multi-module specific sections can be moved into a single ``[[tools.mypy.overrides]]`` section with a + module property set to an array of modules: + + * For example, ``[mypy-packagename,packagename2]`` would become: + +.. code-block:: toml + + [[tool.mypy.overrides]] + module = [ + 'packagename', + 'packagename2' + ] + ... + +* The following care should be given to values in the ``pyproject.toml`` files as compared to ``ini`` files: + + * Strings must be wrapped in double quotes, or single quotes if the string contains special characters + + * Boolean values should be all lower case + +Please see the `TOML Documentation`_ for more details and information on +what is allowed in a ``toml`` file. See `PEP 518`_ for more information on the layout +and structure of the ``pyproject.toml`` file. + +Example ``pyproject.toml`` +************************** + +Here is an example of a ``pyproject.toml`` file. To use this config file, place it at the root +of your repo (or append it to the end of an existing ``pyproject.toml`` file) and run mypy. + +.. code-block:: toml + + # mypy global options: + + [tool.mypy] + python_version = "2.7" + warn_return_any = true + warn_unused_configs = true + + # mypy per-module options: + + [[tool.mypy.overrides]] + module = "mycode.foo.*" + disallow_untyped_defs = true + + [[tool.mypy.overrides]] + module = "mycode.bar" + warn_return_any = false + + [[tool.mypy.overrides]] + module = [ + "somelibrary", + "some_other_library" + ] + ignore_missing_imports = true + .. _lxml: https://pypi.org/project/lxml/ .. _SQLite: https://www.sqlite.org/ +.. _PEP 518: https://www.python.org/dev/peps/pep-0518/ +.. _TOML Documentation: https://toml.io/ diff --git a/mypy/config_parser.py b/mypy/config_parser.py index dd79869030e5..76aa629cbec3 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -1,4 +1,5 @@ import argparse +from collections import OrderedDict import configparser import glob as fileglob from io import StringIO @@ -6,12 +7,17 @@ import re import sys -from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, TextIO +import toml +from typing import (Any, Callable, Dict, List, Mapping, MutableMapping, Optional, Sequence, + TextIO, Tuple, Union, cast) from typing_extensions import Final from mypy import defaults from mypy.options import Options, PER_MODULE_OPTIONS +_CONFIG_VALUE_TYPES = Union[str, bool, int, float, Dict[str, str], List[str], Tuple[int, int]] +_INI_PARSER_CALLABLE = Callable[[Any], _CONFIG_VALUE_TYPES] + def parse_version(v: str) -> Tuple[int, int]: m = re.match(r'\A(\d)\.(\d+)\Z', v) @@ -34,6 +40,14 @@ def parse_version(v: str) -> Tuple[int, int]: return major, minor +def try_split(v: Union[str, Sequence[str]], split_regex: str = '[,]') -> List[str]: + """Split and trim a str or list of str into a list of str""" + if isinstance(v, str): + return [p.strip() for p in re.split(split_regex, v)] + + return [p.strip() for p in v] + + def expand_path(path: str) -> str: """Expand the user home directory and any environment variables contained within the provided path. @@ -42,9 +56,8 @@ def expand_path(path: str) -> str: return os.path.expandvars(os.path.expanduser(path)) -def split_and_match_files(paths: str) -> List[str]: - """Take a string representing a list of files/directories (with support for globbing - through the glob library). +def split_and_match_files_list(paths: Sequence[str]) -> List[str]: + """Take a list of files/directories (with support for globbing through the glob library). Where a path/glob matches no file, we still include the raw path in the resulting list. @@ -52,7 +65,7 @@ def split_and_match_files(paths: str) -> List[str]: """ expanded_paths = [] - for path in paths.split(','): + for path in paths: path = expand_path(path.strip()) globbed_files = fileglob.glob(path, recursive=True) if globbed_files: @@ -63,6 +76,18 @@ def split_and_match_files(paths: str) -> List[str]: return expanded_paths +def split_and_match_files(paths: str) -> List[str]: + """Take a string representing a list of files/directories (with support for globbing + through the glob library). + + Where a path/glob matches no file, we still include the raw path in the resulting list. + + Returns a list of file paths + """ + + return split_and_match_files_list(paths.split(',')) + + def check_follow_imports(choice: str) -> str: choices = ['normal', 'silent', 'skip', 'error'] if choice not in choices: @@ -77,7 +102,7 @@ def check_follow_imports(choice: str) -> str: # sufficient, and we don't have to do anything here. This table # exists to specify types for values initialized to None or container # types. -config_types = { +ini_config_types = { 'python_version': parse_version, 'strict_optional_whitelist': lambda s: s.split(), 'custom_typing_module': str, @@ -100,7 +125,23 @@ def check_follow_imports(choice: str) -> str: 'cache_dir': expand_path, 'python_executable': expand_path, 'strict': bool, -} # type: Final +} # type: Final[Dict[str, _INI_PARSER_CALLABLE]] + +# Reuse the ini_config_types and overwrite the diff +toml_config_types = ini_config_types.copy() # type: Final[Dict[str, _INI_PARSER_CALLABLE]] +toml_config_types.update({ + 'python_version': lambda s: parse_version(str(s)), + 'strict_optional_whitelist': try_split, + 'mypy_path': lambda s: [expand_path(p) for p in try_split(s, '[,:]')], + 'files': lambda s: split_and_match_files_list(try_split(s)), + 'follow_imports': lambda s: check_follow_imports(str(s)), + 'plugins': try_split, + 'always_true': try_split, + 'always_false': try_split, + 'disable_error_code': try_split, + 'enable_error_code': try_split, + 'package_root': try_split, +}) def parse_config_file(options: Options, set_strict_flags: Callable[[], None], @@ -121,14 +162,27 @@ def parse_config_file(options: Options, set_strict_flags: Callable[[], None], else: config_files = tuple(map(os.path.expanduser, defaults.CONFIG_FILES)) - parser = configparser.RawConfigParser() + config_parser = configparser.RawConfigParser() for config_file in config_files: if not os.path.exists(config_file): continue try: - parser.read(config_file) - except configparser.Error as err: + if is_toml(config_file): + toml_data = cast("OrderedDict[str, Any]", + toml.load(config_file, _dict=OrderedDict)) + # Filter down to just mypy relevant toml keys + toml_data = toml_data.get('tool', {}) + if 'mypy' not in toml_data: + continue + toml_data = OrderedDict({'mypy': toml_data['mypy']}) + parser = destructure_overrides(toml_data) # type: MutableMapping[str, Any] + config_types = toml_config_types + else: + config_parser.read(config_file) + parser = config_parser + config_types = ini_config_types + except (toml.TomlDecodeError, configparser.Error, ConfigTOMLValueError) as err: print("%s: %s" % (config_file, err), file=stderr) else: if config_file in defaults.SHARED_CONFIG_FILES and 'mypy' not in parser: @@ -148,16 +202,17 @@ def parse_config_file(options: Options, set_strict_flags: Callable[[], None], else: section = parser['mypy'] prefix = '%s: [%s]: ' % (file_read, 'mypy') - updates, report_dirs = parse_section(prefix, options, set_strict_flags, section, stderr) + updates, report_dirs = parse_section( + prefix, options, set_strict_flags, section, config_types, stderr) for k, v in updates.items(): setattr(options, k, v) options.report_dirs.update(report_dirs) for name, section in parser.items(): if name.startswith('mypy-'): - prefix = '%s: [%s]: ' % (file_read, name) + prefix = get_prefix(file_read, name) updates, report_dirs = parse_section( - prefix, options, set_strict_flags, section, stderr) + prefix, options, set_strict_flags, section, config_types, stderr) if report_dirs: print("%sPer-module sections should not specify reports (%s)" % (prefix, ', '.join(s + '_report' for s in sorted(report_dirs))), @@ -184,9 +239,97 @@ def parse_config_file(options: Options, set_strict_flags: Callable[[], None], options.per_module_options[glob] = updates +def get_prefix(file_read: str, name: str) -> str: + if is_toml(file_read): + module_name_str = 'module = "%s"' % '-'.join(name.split('-')[1:]) + else: + module_name_str = name + + return '%s: [%s]: ' % (file_read, module_name_str) + + +def is_toml(filemame: str) -> bool: + return filemame.lower().endswith('.toml') + + +def destructure_overrides(toml_data: "OrderedDict[str, Any]") -> "OrderedDict[str, Any]": + """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file, + and convert it back to a flatter structure that the existing config_parser can handle. + + E.g. the following pyproject.toml file: + + [[tool.mypy.overrides]] + module = [ + "a.b", + "b.*" + ] + disallow_untyped_defs = true + + [[tool.mypy.overrides]] + module = 'c' + disallow_untyped_defs = false + + Would map to the following config dict that it would have gotten from parsing an equivalent + ini file: + + { + "mypy-a.b": { + disallow_untyped_defs = true, + }, + "mypy-b.*": { + disallow_untyped_defs = true, + }, + "mypy-c": { + disallow_untyped_defs: false, + }, + } + """ + if 'overrides' not in toml_data['mypy']: + return toml_data + + if not isinstance(toml_data['mypy']['overrides'], list): + raise ConfigTOMLValueError("tool.mypy.overrides sections must be an array. Please make " + "sure you are using double brackets like so: [[tool.mypy.overrides]]") + + result = toml_data.copy() + for override in result['mypy']['overrides']: + if 'module' not in override: + raise ConfigTOMLValueError("toml config file contains a [[tool.mypy.overrides]] " + "section, but no module to override was specified.") + + if isinstance(override['module'], str): + modules = [override['module']] + elif isinstance(override['module'], list): + modules = override['module'] + else: + raise ConfigTOMLValueError("toml config file contains a [[tool.mypy.overrides]] " + "section with a module value that is not a string or a list of " + "strings") + + for module in modules: + module_overrides = override.copy() + del module_overrides['module'] + old_config_name = 'mypy-%s' % module + if old_config_name not in result: + result[old_config_name] = module_overrides + else: + for new_key, new_value in module_overrides.items(): + if (new_key in result[old_config_name] and + result[old_config_name][new_key] != new_value): + raise ConfigTOMLValueError("toml config file contains " + "[[tool.mypy.overrides]] sections with conflicting " + "values. Module '%s' has two different values for '%s'" + % (module, new_key)) + result[old_config_name][new_key] = new_value + + del result['mypy']['overrides'] + return result + + def parse_section(prefix: str, template: Options, set_strict_flags: Callable[[], None], - section: Mapping[str, str], + section: Mapping[str, Any], + config_types: Dict[str, Any], stderr: TextIO = sys.stderr ) -> Tuple[Dict[str, object], Dict[str, str]]: """Parse one section of a config file. @@ -211,7 +354,7 @@ def parse_section(prefix: str, template: Options, if key.endswith('_report'): report_type = key[:-7].replace('_', '-') if report_type in defaults.REPORTER_NAMES: - report_dirs[report_type] = section[key] + report_dirs[report_type] = str(section[key]) else: print("%sUnrecognized report type: %s" % (prefix, key), file=stderr) @@ -240,7 +383,10 @@ def parse_section(prefix: str, template: Options, v = None # type: Any try: if ct is bool: - v = section.getboolean(key) # type: ignore[attr-defined] # Until better stub + if isinstance(section, dict): + v = convert_to_boolean(section.get(key)) + else: + v = section.getboolean(key) # type: ignore[attr-defined] # Until better stub if invert: v = not v elif callable(ct): @@ -281,6 +427,17 @@ def parse_section(prefix: str, template: Options, return results, report_dirs +def convert_to_boolean(value: Optional[Any]) -> bool: + """Return a boolean value translating from other types if necessary.""" + if isinstance(value, bool): + return value + if not isinstance(value, str): + value = str(value) + if value.lower() not in configparser.RawConfigParser.BOOLEAN_STATES: + raise ValueError('Not a boolean: %s' % value) + return configparser.RawConfigParser.BOOLEAN_STATES[value.lower()] + + def split_directive(s: str) -> Tuple[List[str], List[str]]: """Split s on commas, except during quoted sections. @@ -362,7 +519,7 @@ def set_strict_flags() -> None: strict_found = True new_sections, reports = parse_section( - '', template, set_strict_flags, parser['dummy'], stderr=stderr) + '', template, set_strict_flags, parser['dummy'], ini_config_types, stderr=stderr) errors.extend((lineno, x) for x in stderr.getvalue().strip().split('\n') if x) if reports: errors.append((lineno, "Reports not supported in inline configuration")) @@ -375,3 +532,17 @@ def set_strict_flags() -> None: sections.update(new_sections) return sections, errors + + +def get_config_module_names(filename: Optional[str], modules: List[str]) -> str: + if not filename or not modules: + return '' + + if not is_toml(filename): + return ", ".join("[mypy-%s]" % module for module in modules) + + return "module = ['%s']" % ("', '".join(sorted(modules))) + + +class ConfigTOMLValueError(ValueError): + pass diff --git a/mypy/defaults.py b/mypy/defaults.py index 9f1c10c02930..7e19bfe4a56f 100644 --- a/mypy/defaults.py +++ b/mypy/defaults.py @@ -7,12 +7,14 @@ PYTHON3_VERSION_MIN = (3, 4) # type: Final CACHE_DIR = '.mypy_cache' # type: Final CONFIG_FILE = ['mypy.ini', '.mypy.ini'] # type: Final +PYPROJECT_CONFIG_FILES = ['pyproject.toml', ] # type: Final SHARED_CONFIG_FILES = ['setup.cfg', ] # type: Final USER_CONFIG_FILES = ['~/.config/mypy/config', '~/.mypy.ini', ] # type: Final if os.environ.get('XDG_CONFIG_HOME'): USER_CONFIG_FILES.insert(0, os.path.join(os.environ['XDG_CONFIG_HOME'], 'mypy/config')) -CONFIG_FILES = CONFIG_FILE + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final +CONFIG_FILES = (CONFIG_FILE + PYPROJECT_CONFIG_FILES + SHARED_CONFIG_FILES + + USER_CONFIG_FILES) # type: Final # This must include all reporters defined in mypy.report. This is defined here # to make reporter names available without importing mypy.report -- this speeds diff --git a/mypy/main.py b/mypy/main.py index 98dacceb7588..2e684a22d6e3 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -23,7 +23,7 @@ from mypy.errors import CompileError from mypy.errorcodes import error_codes from mypy.options import Options, BuildType -from mypy.config_parser import parse_version, parse_config_file +from mypy.config_parser import get_config_module_names, parse_version, parse_config_file from mypy.split_namespace import SplitNamespace from mypy.version import __version__ @@ -103,8 +103,9 @@ def flush_errors(new_messages: List[str], serious: bool) -> None: if options.warn_unused_configs and options.unused_configs and not options.incremental: print("Warning: unused section(s) in %s: %s" % (options.config_file, - ", ".join("[mypy-%s]" % glob for glob in options.per_module_options.keys() - if glob in options.unused_configs)), + get_config_module_names(options.config_file, + [glob for glob in options.per_module_options.keys() + if glob in options.unused_configs])), file=stderr) maybe_write_junit_xml(time.time() - t0, serious, messages, options) @@ -452,7 +453,8 @@ def add_invertible_flag(flag: str, help="Configuration file, must have a [mypy] section " "(defaults to {})".format(', '.join(defaults.CONFIG_FILES))) add_invertible_flag('--warn-unused-configs', default=False, strict_flag=True, - help="Warn about unused '[mypy-]' config sections", + help="Warn about unused '[mypy-]' or '[[tool.mypy.overrides]]' " + "config sections", group=config_group) imports_group = parser.add_argument_group( diff --git a/mypy/options.py b/mypy/options.py index a32586fbfc5c..9e41ad7d310d 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -141,7 +141,7 @@ def __init__(self) -> None: # Warn about unused '# type: ignore' comments self.warn_unused_ignores = False - # Warn about unused '[mypy-] config sections + # Warn about unused '[mypy-]' or '[[tool.mypy.overrides]]' config sections self.warn_unused_configs = False # Files in which to ignore all non-fatal errors diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index f6b36c376180..8a1fb669276e 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -105,7 +105,7 @@ # Special tests for platforms with case-insensitive filesystems. if sys.platform in ('darwin', 'win32'): - typecheck_files.append('check-modules-case.test') + typecheck_files.extend(['check-modules-case.test']) class TypeCheckSuite(DataSuite): diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 85b25cf92545..9fafb1f36cae 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -24,6 +24,7 @@ # Files containing test case descriptions. cmdline_files = [ 'cmdline.test', + 'cmdline.pyproject.test', 'reports.test', 'envvars.test', ] diff --git a/mypy/test/testdiff.py b/mypy/test/testdiff.py index d4617c299b86..9e1e9097152b 100644 --- a/mypy/test/testdiff.py +++ b/mypy/test/testdiff.py @@ -16,7 +16,9 @@ class ASTDiffSuite(DataSuite): - files = ['diff.test'] + files = [ + 'diff.test', + ] def run_case(self, testcase: DataDrivenTestCase) -> None: first_src = '\n'.join(testcase.input) diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index c2737308adc1..f95748db0a24 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -159,7 +159,7 @@ def get_options(self, options.follow_imports = 'error' for name, _ in testcase.files: - if 'mypy.ini' in name: + if 'mypy.ini' in name or 'pyproject.toml' in name: parse_config_file(options, lambda: None, name) break diff --git a/mypy/test/testpep561.py b/mypy/test/testpep561.py index 2d0763141ea4..ba5be621e066 100644 --- a/mypy/test/testpep561.py +++ b/mypy/test/testpep561.py @@ -35,7 +35,9 @@ class PEP561Suite(DataSuite): - files = ['pep561.test'] + files = [ + 'pep561.test', + ] def run_case(self, test_case: DataDrivenTestCase) -> None: test_pep561(test_case) @@ -136,6 +138,9 @@ def test_pep561(testcase: DataDrivenTestCase) -> None: if 'mypy.ini' in name: with open('mypy.ini', 'w') as m: m.write(content) + elif 'pyproject.toml' in name: + with open('pyproject.toml', 'w') as m: + m.write(content) output = [] # Type check the module out, err, returncode = mypy.api.run(cmd_line) diff --git a/test-data/unit/check-custom-plugin.test b/test-data/unit/check-custom-plugin.test index 580310e70af7..c66c9f36a260 100644 --- a/test-data/unit/check-custom-plugin.test +++ b/test-data/unit/check-custom-plugin.test @@ -11,6 +11,14 @@ reveal_type(f()) # N: Revealed type is "builtins.int" \[mypy] plugins=/test-data/unit/plugins/fnplugin.py +[case testFunctionPluginFilePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +def f() -> str: ... +reveal_type(f()) # N: Revealed type is "builtins.int" +[file pyproject.toml] +\[tool.mypy] +plugins='/test-data/unit/plugins/fnplugin.py' + [case testFunctionPlugin] # flags: --config-file tmp/mypy.ini def f() -> str: ... @@ -19,6 +27,14 @@ reveal_type(f()) # N: Revealed type is "builtins.int" \[mypy] plugins=fnplugin +[case testFunctionPluginPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +def f() -> str: ... +reveal_type(f()) # N: Revealed type is "builtins.int" +[file pyproject.toml] +\[tool.mypy] +plugins = 'fnplugin' + [case testFunctionPluginFullnameIsNotNone] # flags: --config-file tmp/mypy.ini from typing import Callable, TypeVar @@ -30,6 +46,17 @@ g(f)() \[mypy] plugins=/test-data/unit/plugins/fnplugin.py +[case testFunctionPluginFullnameIsNotNonePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +from typing import Callable, TypeVar +f: Callable[[], None] +T = TypeVar('T') +def g(x: T) -> T: return x # This strips out the name of a callable +g(f)() +[file pyproject.toml] +\[tool.mypy] +plugins="/test-data/unit/plugins/fnplugin.py" + [case testTwoPlugins] # flags: --config-file tmp/mypy.ini def f(): ... @@ -43,6 +70,20 @@ reveal_type(h()) # N: Revealed type is "Any" plugins=/test-data/unit/plugins/fnplugin.py, /test-data/unit/plugins/plugin2.py +[case testTwoPluginsPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +def f(): ... +def g(): ... +def h(): ... +reveal_type(f()) # N: Revealed type is "builtins.int" +reveal_type(g()) # N: Revealed type is "builtins.str" +reveal_type(h()) # N: Revealed type is "Any" +[file pyproject.toml] +\[tool.mypy] +plugins=['/test-data/unit/plugins/fnplugin.py', + '/test-data/unit/plugins/plugin2.py' +] + [case testTwoPluginsMixedType] # flags: --config-file tmp/mypy.ini def f(): ... @@ -55,6 +96,18 @@ reveal_type(h()) # N: Revealed type is "Any" \[mypy] plugins=/test-data/unit/plugins/fnplugin.py, plugin2 +[case testTwoPluginsMixedTypePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +def f(): ... +def g(): ... +def h(): ... +reveal_type(f()) # N: Revealed type is "builtins.int" +reveal_type(g()) # N: Revealed type is "builtins.str" +reveal_type(h()) # N: Revealed type is "Any" +[file pyproject.toml] +\[tool.mypy] +plugins=['/test-data/unit/plugins/fnplugin.py', 'plugin2'] + [case testMissingPluginFile] # flags: --config-file tmp/mypy.ini [file mypy.ini] diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index aa6b54068aa8..7099f5b0ec19 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -478,6 +478,24 @@ disallow_incomplete_defs = False disallow_incomplete_defs = True +[case testPerFileIncompleteDefsBasicPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import standard, incomplete + +[file standard.py] +def incomplete(x) -> int: + return 0 +[file incomplete.py] +def incomplete(x) -> int: # E: Function is missing a type annotation for one or more arguments + return 0 +[file pyproject.toml] +\[tool.mypy] +disallow_incomplete_defs = false +\[[tool.mypy.overrides]] +module = 'incomplete' +disallow_incomplete_defs = true + + [case testPerFileStrictOptionalBasic] # flags: --config-file tmp/mypy.ini import standard, optional @@ -498,6 +516,27 @@ strict_optional = False strict_optional = True +[case testPerFileStrictOptionalBasicPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import standard, optional + +[file standard.py] +x = 0 +if int(): + x = None +[file optional.py] +x = 0 +if int(): + x = None # E: Incompatible types in assignment (expression has type "None", variable has type "int") + +[file pyproject.toml] +\[tool.mypy] +strict_optional = false +\[[tool.mypy.overrides]] +module = 'optional' +strict_optional = true + + [case testPerFileStrictOptionalBasicImportStandard] # flags: --config-file tmp/mypy.ini import standard, optional @@ -525,6 +564,34 @@ strict_optional = False strict_optional = True +[case testPerFileStrictOptionalBasicImportStandardPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import standard, optional + +[file standard.py] +from typing import Optional +def f(x: int) -> None: pass +an_int = 0 # type: int +optional_int = None # type: Optional[int] +f(an_int) # ints can be used as ints +f(optional_int) # optional ints can be used as ints in this file + +[file optional.py] +import standard +def f(x: int) -> None: pass +standard.an_int = None # E: Incompatible types in assignment (expression has type "None", variable has type "int") +standard.optional_int = None # OK -- explicitly declared as optional +f(standard.an_int) # ints can be used as ints +f(standard.optional_int) # E: Argument 1 to "f" has incompatible type "None"; expected "int" + +[file pyproject.toml] +\[tool.mypy] +strict_optional = false +\[[tool.mypy.overrides]] +module = 'optional' +strict_optional = true + + [case testPerFileStrictOptionalBasicImportOptional] # flags: --config-file tmp/mypy.ini import standard, optional @@ -547,6 +614,31 @@ strict_optional = False \[mypy-optional] strict_optional = True + +[case testPerFileStrictOptionalBasicImportOptionalPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import standard, optional + +[file standard.py] +import optional +def f(x: int) -> None: pass +f(optional.x) # OK -- in non-strict Optional context +f(optional.y) # OK -- in non-strict Optional context + +[file optional.py] +from typing import Optional +def f(x: int) -> None: pass +x = 0 # type: Optional[int] +y = None # type: None + +[file pyproject.toml] +\[tool.mypy] +strict_optional = false +\[[tool.mypy.overrides]] +module = 'optional' +strict_optional = true + + [case testPerFileStrictOptionalListItemImportOptional] # flags: --config-file tmp/mypy.ini import standard, optional @@ -571,6 +663,34 @@ strict_optional = False strict_optional = True [builtins fixtures/list.pyi] + +[case testPerFileStrictOptionalListItemImportOptionalPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import standard, optional + +[file standard.py] +import optional +from typing import List +def f(x: List[int]) -> None: pass +f(optional.x) # OK -- in non-strict Optional context +f(optional.y) # OK -- in non-strict Optional context + +[file optional.py] +from typing import Optional, List +def f(x: List[int]) -> None: pass +x = [] # type: List[Optional[int]] +y = [] # type: List[int] + +[file pyproject.toml] +\[tool.mypy] +strict_optional = false +\[[tool.mypy.overrides]] +module = 'optional' +strict_optional = true + +[builtins fixtures/list.pyi] + + [case testPerFileStrictOptionalComplicatedList] from typing import Union, Optional, List @@ -596,6 +716,27 @@ strict_optional = False \[mypy-optional] strict_optional = True + +[case testPerFileStrictOptionalNoneArgumentsPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import standard, optional + +[file standard.py] +def f(x: int = None) -> None: pass + +[file optional.py] +import standard +def f(x: int = None) -> None: pass +standard.f(None) + +[file pyproject.toml] +\[tool.mypy] +strict_optional = false +\[[tool.mypy.overrides]] +module = 'optional' +strict_optional = true + + [case testDisallowImplicitTypesIgnoreMissingTypes] # flags: --ignore-missing-imports --disallow-any-unimported from missing import MyType @@ -1078,6 +1219,24 @@ always_true = YOLO1, YOLO always_false = BLAH, BLAH1 [builtins fixtures/bool.pyi] + +[case testAlwaysTrueAlwaysFalseConfigFilePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +from somewhere import YOLO, BLAH +if not YOLO: + 1+() +if BLAH: + 1+() + +[file pyproject.toml] +\[tool.mypy] +ignore_missing_imports = true +always_true = ['YOLO1', 'YOLO'] +always_false = ['BLAH', 'BLAH1'] + +[builtins fixtures/bool.pyi] + + [case testDisableErrorCodeConfigFile] # flags: --config-file tmp/mypy.ini --disallow-untyped-defs import foo @@ -1087,6 +1246,18 @@ def bar(): \[mypy] disable_error_code = import, no-untyped-def + +[case testDisableErrorCodeConfigFilePyProjectTOML] +# flags: --config-file tmp/pyproject.toml --disallow-untyped-defs +import foo +def bar(): + pass + +[file pyproject.toml] +\[tool.mypy] +disable_error_code = ['import', 'no-untyped-def'] + + [case testCheckDisallowAnyGenericsNamedTuple] # flags: --disallow-any-generics from typing import NamedTuple @@ -1190,6 +1361,26 @@ def f(c: A) -> None: # E: Missing type parameters for generic type "A" strict = True [out] + +[case testStrictInConfigAnyGenericPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +from typing import TypeVar, Generic + +T = TypeVar('T') + +class A(Generic[T]): + pass + +def f(c: A) -> None: # E: Missing type parameters for generic type "A" + pass + +[file pyproject.toml] +\[tool.mypy] +strict = true + +[out] + + [case testStrictFalseInConfigAnyGeneric] # flags: --config-file tmp/mypy.ini from typing import TypeVar, Generic @@ -1206,6 +1397,26 @@ def f(c: A) -> None: strict = False [out] + +[case testStrictFalseInConfigAnyGenericPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +from typing import TypeVar, Generic + +T = TypeVar('T') + +class A(Generic[T]): + pass + +def f(c: A) -> None: + pass + +[file pyproject.toml] +\[tool.mypy] +strict = false + +[out] + + [case testStrictAndStrictEquality] # flags: --strict x = 0 @@ -1227,6 +1438,25 @@ strict_equality = True strict_equality = False [builtins fixtures/bool.pyi] + +[case testStrictEqualityPerFilePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import b +42 == 'no' # E: Non-overlapping equality check (left operand type: "Literal[42]", right operand type: "Literal['no']") + +[file b.py] +42 == 'no' + +[file pyproject.toml] +\[tool.mypy] +strict_equality = true +\[[tool.mypy.overrides]] +module = 'b' +strict_equality = false + +[builtins fixtures/bool.pyi] + + [case testNoImplicitReexport] # flags: --no-implicit-reexport from other_module_2 import a @@ -1305,6 +1535,28 @@ implicit_reexport = False [out] main:2: error: Module "other_module_2" has no attribute "a" + +[case testNoImplicitReexportPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +from other_module_2 import a + +[file other_module_1.py] +a = 5 + +[file other_module_2.py] +from other_module_1 import a + +[file pyproject.toml] +\[tool.mypy] +implicit_reexport = true +\[[tool.mypy.overrides]] +module = 'other_module_2' +implicit_reexport = false + +[out] +main:2: error: Module "other_module_2" has no attribute "a" + + [case testImplicitAnyOKForNoArgs] # flags: --disallow-any-generics --show-column-numbers from typing import List @@ -1539,6 +1791,34 @@ disallow_subclassing_any = True \[mypy-m] disallow_subclassing_any = False + +[case testDisallowSubclassingAnyPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import m +import y + +[file m.py] +from typing import Any + +x = None # type: Any + +class ShouldBeFine(x): ... + +[file y.py] +from typing import Any + +x = None # type: Any + +class ShouldNotBeFine(x): ... # E: Class cannot subclass 'x' (has type 'Any') + +[file pyproject.toml] +\[tool.mypy] +disallow_subclassing_any = true +\[[tool.mypy.overrides]] +module = 'm' +disallow_subclassing_any = false + + [case testNoImplicitOptionalPerModule] # flags: --config-file tmp/mypy.ini import m @@ -1553,6 +1833,23 @@ no_implicit_optional = True \[mypy-m] no_implicit_optional = False + +[case testNoImplicitOptionalPerModulePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import m + +[file m.py] +def f(a: str = None) -> int: + return 0 + +[file pyproject.toml] +\[tool.mypy] +no_implicit_optional = true +\[[tool.mypy.overrides]] +module = 'm' +no_implicit_optional = false + + [case testNoImplicitOptionalPerModulePython2] # flags: --config-file tmp/mypy.ini --python-version 2.7 import m @@ -1568,6 +1865,24 @@ no_implicit_optional = True \[mypy-m] no_implicit_optional = False + +[case testNoImplicitOptionalPerModulePython2PyProjectTOML] +# flags: --config-file tmp/pyproject.toml --python-version 2.7 +import m + +[file m.py] +def f(a = None): + # type: (str) -> int + return 0 + +[file pyproject.toml] +\[tool.mypy] +no_implicit_optional = true +\[[tool.mypy.overrides]] +module = 'm' +no_implicit_optional = false + + [case testDisableErrorCode] # flags: --disable-error-code attr-defined x = 'should be fine' diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 90a13c23132b..076757110c2f 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -1812,6 +1812,30 @@ main:3: note: Revealed type is "builtins.int" [out2] main:3: note: Revealed type is "Any" + +[case testIncrementalFollowImportsVariablePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import a +reveal_type(a.x) + +[file a.py] +x = 0 + +[file pyproject.toml] +\[tool.mypy] +follow_imports = 'normal' + +[file pyproject.toml.2] +\[tool.mypy] +follow_imports = 'skip' + +[out1] +main:3: note: Revealed type is "builtins.int" + +[out2] +main:3: note: Revealed type is "Any" + + [case testIncrementalNamedTupleInMethod] from ntcrash import nope [file ntcrash.py] diff --git a/test-data/unit/check-modules-case.test b/test-data/unit/check-modules-case.test index ebe0e31674b2..bc2e91aacfdc 100644 --- a/test-data/unit/check-modules-case.test +++ b/test-data/unit/check-modules-case.test @@ -22,6 +22,26 @@ x = 1 \[mypy] mypy_path = tmp/funky_case + +[case testCaseInsensitivityDirPyProjectTOML] +# flags: --config-file tmp/pyproject.toml + +from a import B # E: Module "a" has no attribute "B" +from other import x +reveal_type(x) # N: Revealed type is "builtins.int" + +[file a/__init__.py] + +[file a/b/__init__.py] + +[file FuNkY_CaSe/other.py] +x = 1 + +[file pyproject.toml] +\[tool.mypy] +mypy_path = "tmp/funky_case" + + [case testPreferPackageOverFileCase] # flags: --config-file tmp/mypy.ini import a @@ -34,6 +54,22 @@ pass \[mypy] mypy_path = tmp/funky + +[case testPreferPackageOverFileCasePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import a + +[file funky/a.py] +/ # Deliberate syntax error, this file should not be parsed. + +[file FuNkY/a/__init__.py] +pass + +[file pyproject.toml] +\[tool.mypy] +mypy_path = "tmp/funky" + + [case testNotPreferPackageOverFileCase] import a [file a.py] @@ -53,6 +89,23 @@ x = '' \[mypy] mypy_path = tmp/xx, tmp/yy + +[case testNamespacePackagePickFirstOnMypyPathCasePyProjectTOML] +# flags: --namespace-packages --config-file tmp/pyproject.toml +from foo.bar import x +reveal_type(x) # N: Revealed type is "builtins.int" + +[file XX/foo/bar.py] +x = 0 + +[file yy/foo/bar.py] +x = '' + +[file pyproject.toml] +\[tool.mypy] +mypy_path = ["tmp/xx", "tmp/yy"] + + [case testClassicPackageInsideNamespacePackageCase] # flags: --namespace-packages --config-file tmp/mypy.ini from foo.bar.baz.boo import x @@ -67,3 +120,23 @@ x = 0 [file mypy.ini] \[mypy] mypy_path = TmP/xX, TmP/yY + + +[case testClassicPackageInsideNamespacePackageCasePyProjectTOML] +# flags: --namespace-packages --config-file tmp/pyproject.toml +from foo.bar.baz.boo import x +reveal_type(x) # N: Revealed type is "builtins.int" + +[file xx/foo/bar/baz/boo.py] +x = '' + +[file xx/foo/bar/baz/__init__.py] + +[file yy/foo/bar/baz/boo.py] +x = 0 + +[file yy/foo/bar/__init__.py] + +[file pyproject.toml] +\[tool.mypy] +mypy_path = ["TmP/xX", "TmP/yY"] diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 824ff96e5fa2..559b3868ea9b 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2392,6 +2392,44 @@ ignore_missing_imports = True main:3: error: Cannot find implementation or library stub for module named "a.b.d" main:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports + +[case testModuleGetattrInit10PyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import a.b.c # silenced +import a.b.d # error + +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: ... + +[file a/b/__init__.pyi] +# empty (i.e. complete subpackage) + +[file pyproject.toml] +\[tool.mypy] +\[[tool.mypy.overrides]] +module = 'a.b.c' +ignore_missing_imports = true + +[builtins fixtures/module.pyi] + +[out] +main:3: error: Cannot find implementation or library stub for module named "a.b.d" +main:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports + + +[case testMultipleModulesInOverridePyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import a +import b + +[file pyproject.toml] +\[tool.mypy] +\[[tool.mypy.overrides]] +module = ['a', 'b'] +ignore_missing_imports = true + + [case testIndirectFromImportWithinCycleUsedAsBaseClass] import a [file a.py] @@ -2649,6 +2687,30 @@ z = 0 \[mypy] mypy_path = tmp/xx, tmp/yy + +[case testNamespacePackageWithMypyPathPyProjectTOML] +# flags: --namespace-packages --config-file tmp/pyproject.toml +from foo.bax import x +from foo.bay import y +from foo.baz import z +reveal_type(x) # N: Revealed type is "builtins.int" +reveal_type(y) # N: Revealed type is "builtins.int" +reveal_type(z) # N: Revealed type is "builtins.int" + +[file xx/foo/bax.py] +x = 0 + +[file yy/foo/bay.py] +y = 0 + +[file foo/baz.py] +z = 0 + +[file pyproject.toml] +\[tool.mypy] +mypy_path = ["tmp/xx", "tmp/yy"] + + [case testClassicPackageIgnoresEarlierNamespacePackage] # flags: --namespace-packages --config-file tmp/mypy.ini from foo.bar import y diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test index d2061a2594a3..bf997169b2b5 100644 --- a/test-data/unit/check-newsemanal.test +++ b/test-data/unit/check-newsemanal.test @@ -1764,6 +1764,38 @@ strict_optional = True \[mypy-b] strict_optional = False + +[case testNewAnalyzerTypeArgBoundCheckWithStrictOptionalPyProjectTOML] +# flags: --config-file tmp/pyproject.toml +import a + +[file b.py] +from typing import TypeVar, Generic + +x: C[None] +y: C[str] # E: Type argument "builtins.str" of "C" must be a subtype of "builtins.int" +z: C[int] + +T = TypeVar('T', bound=int) +class C(Generic[T]): + pass + +[file a.py] +from b import C + +x: C[None] # E: Type argument "None" of "C" must be a subtype of "builtins.int" +y: C[str] # E: Type argument "builtins.str" of "C" must be a subtype of "builtins.int" +z: C[int] + +[file pyproject.toml] +\[[tool.mypy.overrides]] +module = 'a' +strict_optional = true +\[[tool.mypy.overrides]] +module = 'b' +strict_optional = false + + [case testNewAnalyzerProperty] class A: @property diff --git a/test-data/unit/cmdline.pyproject.test b/test-data/unit/cmdline.pyproject.test new file mode 100644 index 000000000000..83f4745d0786 --- /dev/null +++ b/test-data/unit/cmdline.pyproject.test @@ -0,0 +1,82 @@ +-- Tests for command line parsing +-- ------------------------------ +-- +-- The initial line specifies the command line, in the format +-- +-- # cmd: mypy +-- +-- Note that # flags: --some-flag IS NOT SUPPORTED. +-- Use # cmd: mypy --some-flag ... +-- +-- '== Return code: ' is added to the output when the process return code +-- is "nonobvious" -- that is, when it is something other than 0 if there are no +-- messages and 1 if there are. + +-- Directories/packages on the command line +-- ---------------------------------------- + +[case testNonArrayOverridesPyprojectTOML] +# cmd: mypy x.py +[file pyproject.toml] +\[tool.mypy] +\[tool.mypy.overrides] +module = "x" +disallow_untyped_defs = false +[file x.py] +def f(a): + pass +def g(a: int) -> int: + return f(a) +[out] +pyproject.toml: tool.mypy.overrides sections must be an array. Please make sure you are using double brackets like so: [[tool.mypy.overrides]] +== Return code: 0 + +[case testNoModuleInOverridePyprojectTOML] +# cmd: mypy x.py +[file pyproject.toml] +\[tool.mypy] +\[[tool.mypy.overrides]] +disallow_untyped_defs = false +[file x.py] +def f(a): + pass +def g(a: int) -> int: + return f(a) +[out] +pyproject.toml: toml config file contains a [[tool.mypy.overrides]] section, but no module to override was specified. +== Return code: 0 + +[case testInvalidModuleInOverridePyprojectTOML] +# cmd: mypy x.py +[file pyproject.toml] +\[tool.mypy] +\[[tool.mypy.overrides]] +module = 0 +disallow_untyped_defs = false +[file x.py] +def f(a): + pass +def g(a: int) -> int: + return f(a) +[out] +pyproject.toml: toml config file contains a [[tool.mypy.overrides]] section with a module value that is not a string or a list of strings +== Return code: 0 + +[case testConflictingModuleInOverridesPyprojectTOML] +# cmd: mypy x.py +[file pyproject.toml] +\[tool.mypy] +\[[tool.mypy.overrides]] +module = 'x' +disallow_untyped_defs = false +\[[tool.mypy.overrides]] +module = ['x'] +disallow_untyped_defs = true +[file x.py] +def f(a): + pass +def g(a: int) -> int: + return f(a) +[out] +pyproject.toml: toml config file contains [[tool.mypy.overrides]] sections with conflicting values. Module 'x' has two different values for 'disallow_untyped_defs' +== Return code: 0