Repository: python/mypy
Pull request: Add support for pulling configuration from pyproject.toml files
### Description

<!--
If this pull request closes or fixes an issue, write Closes #NNN" or "Fixes #NNN" in that exact
format.
-->

Closes #5205 

This PR will add support to mypy for end users specifying configuration in a pyproject.toml file. . I also updated the documentation to indicate that this support has been added, along with some guidelines on adapting the ini configurations to the toml format.

## Test Plan

<!--
If this is a documentation change, rebuild the docs (link to instructions) and review the changed pages for markup errors.
If this is a code change, include new tests (link to the testing docs). Be sure to run the tests locally and fix any errors before submitting the PR (more instructions).
If this change cannot be tested by the CI, please explain how to verify it manually.
-->

I've added tests as best I could to mirror existing tests that use a mypy.ini file, but with a pyrproject.toml file, to ensure the configuration is being pulled correctly with the desired results.


Problem statement:
Support for pyproject.toml
PEP518 says that `pyproject.toml` is to support the minimum build requirements. I want to consider mypy as part of the minimum build on my package.

For example Pip uses a `[tool.towncrier]` section on `pyproject.toml` to generate a correctly updated `NEWS.rst` as part of its build. One can considers typing correctness as important to the distributed source pakcage as an well documented `NEWS.rst`. Other example is Black that also has it section on `pyproject.toml`.

Maybe you think that this is a silly feature request and that `setup.cfg` must be used instead, Pip or Flit or whatever can setup mypy after all. But an TOML module will be added to the standard library sooner or later, than could mypy accept this feature and antecipate TOML supporting `pyproject.toml` today?

Hints from issue discussion:
PEP 518 is accepted and specifies that `pyproject.toml` is preferred over `setup.cfg`, so I think we should support whatever the PEP specifies.
Does adding pyproject.toml mean that we can remove other config files?
Until that point I'm not excited.

I think that removing other config files would simplify config parsing even more. I would be very glad to change `mypy.main.parse_config_file()` accordingly.

If you agree that `mypy==0.620` it is still in its early ages, would be exciting to remove the other config files. Otherwise I'm already coding the unexciting changes to support both legacy and `pyproject.toml`.

There is already lots of deprecated options:

```
--disallow-any
--strict-boolean
--dirty-stub
--use-python-pat
--silent-import
--almost-silen
--fast-parser
--no-fast-parser
``` 

So legacy config files (`mypy.ini`, `setup.cfg` and `~/.mypy.ini`) could also be one of them.
Note that mypy.ini serves a completely different purpose from setup.py, setup.cfg, and pyproject.toml. The mypy.ini file is read by mypy when it is about to type-check another program. The other files (IIUC) are used to build mypy itself. I don't think we can change mypy.ini much, though we *can* start removing deprecated flags (after they have been printing a warning for 1-2 release cycles). Please do that in a separate PR.
Even if `setup.cfg` was created originally for distutils or setuptools, everybody else seems to use it as an generic configuration file. PyTest for example don't care if is running from `setup.py` or whatever, it simply reads `setup.cfg` if it is found; many tools seems to do it (eg flake8, isort and even mypy).

If you are talking about to replace `setup.py` by `pyproject.toml`, yes it is indeed an legibility win.

Now taking about mypy config files when type-checking other programs, they are all equivalents and fallbacks of the next one (`--config-file` -> `mypy.ini`  -> `setup.cfg` ->  `~/.mypy.ini`; http://mypy.readthedocs.io/en/latest/config_file.html. I'm talking to deprecate these ini-like config files and use toml, it is even more typed!

I will submit a PR  to remove that flags later on.


To put it clear: enable sections like `[tool.mypy]` or `[tool.mypy-somepackage]` in `pyproject.toml`.
Switching mypy.ini to a toml thing sounds like scope creep and we won't have the time and motivation to review PRs related to this, so please don't start on that. The rest is fine of course.
Seems that I misunderstood that to remove others would be necessary to the overral comunity need to target the same. Sorry.

> Does adding pyproject.toml mean that we can remove other config files? Until that point I'm not excited.
So what exactly is being proposed here?

There's a few things I can think of:
- Add support for reading mypy's configuration out of `pyproject.toml`. In addition, we should still support reading configs out of `setup.cfg` and `mypy.ini` for backward compatibility. We could perhaps deprecate `setup.cfg` support though.
- Move configurations within mypy's own repo to `pyproject.toml`. This could replace the current `pytest.ini` and `setup.cfg` files. Not sure whether all current tools that read these config files support `pyproject.toml` yet though.
Pytest seems to be stalled until someone works on it https://github.com/pytest-dev/pytest/issues/1556

An issue for flake8 was opened yesterday https://gitlab.com/pycqa/flake8/issues/428

And the same for coverage https://bitbucket.org/ned/coveragepy/issues/664/allow-configuration-via-pyprojecttoml

I think we should add support for reading mypy's configuration from pyproject.toml, but it looks like we will need to wait to use it for our own config.

Commit message: 3.5 doesn't have typing.Type?

Patch to review:
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..308fcd2dbdde 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 <mypy --config-file>` command-line flag can be used
 to read a different file instead (see :ref:`config-file-flag`).
@@ -885,5 +885,34 @@ 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:
+
+* All ``[mypy]`` sections should have ``tool.`` prepended to their name:
+
+  * ``[mypy]`` would become ``[tool.mypy]``
+
+  * For example, ``[mypy-packagename]`` would become ``[tool.mypy-packagename]``
+
+* Any section with special characters (such as ``.`` or ``*``) will need to be wrapped in single quotes:
+
+  * For example, ``[tool.mypy-package.*]`` would become ``[tool.'mypy-package.*']``
+
+* 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.
+
 .. _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..0393fc81f5a9 100644
--- a/mypy/config_parser.py
+++ b/mypy/config_parser.py
@@ -1,12 +1,14 @@
 import argparse
 import configparser
+import copy
 import glob as fileglob
 from io import StringIO
 import os
 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, Optional, Sequence, TextIO, Tuple, Union
 from typing_extensions import Final
 
 from mypy import defaults
@@ -34,6 +36,15 @@ 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 +53,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 +62,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 +73,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 +99,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 +122,22 @@ def check_follow_imports(choice: str) -> str:
     'cache_dir': expand_path,
     'python_executable': expand_path,
     'strict': bool,
-}  # type: Final
+}  # type: Final[Dict[str, Any]]
+
+# Reuse the ini_config_types and overwrite the diff
+toml_config_types = copy.deepcopy(ini_config_types)  # type: Final[Dict[str, Any]]
+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)),
+    '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 +158,26 @@ 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 config_file.endswith('.toml'):
+                toml_data = toml.load(config_file)
+                # Filter down to just mypy relevant toml keys
+                parser = {key: value
+                          for key, value in toml_data.get('tool', {}).items()
+                          if key.split('-')[0] == 'mypy'}
+                if parser.get('mypy') is None:
+                    continue
+                config_types = toml_config_types
+            else:
+                config_parser.read(config_file)
+                parser = {key: value for key, value in config_parser.items()}
+                config_types = ini_config_types
+        except (toml.TomlDecodeError, configparser.Error) 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,7 +197,8 @@ 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)
@@ -157,7 +207,7 @@ def parse_config_file(options: Options, set_strict_flags: Callable[[], None],
         if name.startswith('mypy-'):
             prefix = '%s: [%s]: ' % (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))),
@@ -187,6 +237,7 @@ def parse_config_file(options: Options, set_strict_flags: Callable[[], None],
 def parse_section(prefix: str, template: Options,
                   set_strict_flags: Callable[[], None],
                   section: Mapping[str, str],
+                  config_types: Dict[str, Any],
                   stderr: TextIO = sys.stderr
                   ) -> Tuple[Dict[str, object], Dict[str, str]]:
     """Parse one section of a config file.
@@ -240,7 +291,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 +335,18 @@ 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 +428,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"))
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/test/testcheck.py b/mypy/test/testcheck.py
index 51f5d71c12ad..4775fa1692a3 100644
--- a/mypy/test/testcheck.py
+++ b/mypy/test/testcheck.py
@@ -41,6 +41,7 @@
     'check-multiple-inheritance.test',
     'check-super.test',
     'check-modules.test',
+    'check-modules.pyproject.test',
     'check-typevar-values.test',
     'check-unsupported.test',
     'check-unreachable-code.test',
@@ -55,10 +56,13 @@
     'check-type-promotion.test',
     'check-semanal-error.test',
     'check-flags.test',
+    'check-flags.pyproject.test',
     'check-incremental.test',
+    'check-incremental.pyproject.test',
     'check-serialize.test',
     'check-bound.test',
     'check-optional.test',
+    'check-optional.pyproject.test',
     'check-fastparse.test',
     'check-warnings.test',
     'check-async-await.test',
@@ -79,6 +83,7 @@
     'check-enum.test',
     'check-incomplete-fixture.test',
     'check-custom-plugin.test',
+    'check-custom-pyproject-plugin.test',
     'check-default-plugin.test',
     'check-attr.test',
     'check-ctypes.test',
@@ -104,7 +109,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', 'check-modules-case.pyproject.test'])
 
 
 class TypeCheckSuite(DataSuite):
diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py
index 85b25cf92545..ec2972fe4001 100644
--- a/mypy/test/testcmdline.py
+++ b/mypy/test/testcmdline.py
@@ -24,7 +24,9 @@
 # Files containing test case descriptions.
 cmdline_files = [
     'cmdline.test',
+    'cmdline.pyproject.test',
     'reports.test',
+    'reports.pyproject.test',
     'envvars.test',
 ]
 
diff --git a/mypy/test/testdaemon.py b/mypy/test/testdaemon.py
index 641bd8a70372..fe1e27e6098b 100644
--- a/mypy/test/testdaemon.py
+++ b/mypy/test/testdaemon.py
@@ -23,6 +23,7 @@
 # Files containing test cases descriptions.
 daemon_files = [
     'daemon.test',
+    'daemon.pyproject.test',
 ]
 
 
diff --git a/mypy/test/testdiff.py b/mypy/test/testdiff.py
index d4617c299b86..0b370414d06f 100644
--- a/mypy/test/testdiff.py
+++ b/mypy/test/testdiff.py
@@ -16,7 +16,10 @@
 
 
 class ASTDiffSuite(DataSuite):
-    files = ['diff.test']
+    files = [
+        'diff.test',
+        'diff.pyproject.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..264ab04726c0 100644
--- a/mypy/test/testfinegrained.py
+++ b/mypy/test/testfinegrained.py
@@ -45,6 +45,7 @@
 class FineGrainedSuite(DataSuite):
     files = [
         'fine-grained.test',
+        'fine-grained.pyproject.test',
         'fine-grained-cycles.test',
         'fine-grained-blockers.test',
         'fine-grained-modules.test',
@@ -159,7 +160,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..7744c271c8bc 100644
--- a/mypy/test/testpep561.py
+++ b/mypy/test/testpep561.py
@@ -35,7 +35,10 @@
 
 
 class PEP561Suite(DataSuite):
-    files = ['pep561.test']
+    files = [
+        'pep561.test',
+        'pep561.pyproject.test'
+    ]
 
     def run_case(self, test_case: DataDrivenTestCase) -> None:
         test_pep561(test_case)
@@ -136,6 +139,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-pyproject-plugin.test b/test-data/unit/check-custom-pyproject-plugin.test
new file mode 100644
index 000000000000..4dfc13824018
--- /dev/null
+++ b/test-data/unit/check-custom-pyproject-plugin.test
@@ -0,0 +1,737 @@
+-- Test cases for user-defined plugins
+--
+-- Note: Plugins used by tests live under test-data/unit/plugins. Defining
+--       plugin files in test cases does not work reliably.
+
+[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 = '<ROOT>/test-data/unit/plugins/fnplugin.py'
+
+[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 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 = '<ROOT>/test-data/unit/plugins/fnplugin.py'
+
+[case testTwoPluginsPyprojetTOMLPyprojectTOML]
+# 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=[
+     '<ROOT>/test-data/unit/plugins/fnplugin.py',
+     '<ROOT>/test-data/unit/plugins/plugin2.py'
+]
+
+[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 = [
+     '<ROOT>/test-data/unit/plugins/fnplugin.py',
+    "plugin2"
+]
+
+[case testMissingPluginFilePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+[file pyproject.toml]
+\[tool.mypy]
+plugins = "missing.py"
+[out]
+tmp/pyproject.toml:1: error: Can't find plugin 'tmp/missing.py'
+--' (work around syntax highlighting)
+
+[case testMissingPluginPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+[file pyproject.toml]
+\[tool.mypy]
+plugins = "missing"
+[out]
+tmp/pyproject.toml:1: error: Error importing plugin 'missing': No module named 'missing'
+
+[case testMultipleSectionsDefinePluginPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+[file pyproject.toml]
+\[acme]
+plugins = "acmeplugin"
+\[tool.mypy]
+plugins = "missing.py"
+\[another]
+plugins = "another_plugin"
+[out]
+tmp/pyproject.toml:1: error: Can't find plugin 'tmp/missing.py'
+--' (work around syntax highlighting)
+
+[case testInvalidPluginExtensionPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+[file pyproject.toml]
+\[tool.mypy]
+plugins = "dir/badext.pyi"
+[file dir/badext.pyi]
+[out]
+tmp/pyproject.toml:1: error: Plugin 'badext.pyi' does not have a .py extension
+
+[case testMissingPluginEntryPointPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+[file pyproject.toml]
+\[tool.mypy]
+ plugins = '<ROOT>/test-data/unit/plugins/noentry.py'
+[out]
+tmp/pyproject.toml:1: error: Plugin '<ROOT>/test-data/unit/plugins/noentry.py' does not define entry point function "plugin"
+
+[case testCustomPluginEntryPointFilePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+def f() -> str: ...
+reveal_type(f())  # N: Revealed type is 'builtins.int'
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/customentry.py:register'
+
+[case testCustomPluginEntryPointPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+def f() -> str: ...
+reveal_type(f())  # N: Revealed type is 'builtins.int'
+[file pyproject.toml]
+\[tool.mypy]
+plugins = "customentry:register"
+
+[case testInvalidPluginEntryPointReturnValuePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+def f(): pass
+f()
+[file pyproject.toml]
+\[tool.mypy]
+
+plugins = '<ROOT>/test-data/unit/plugins/badreturn.py'
+[out]
+tmp/pyproject.toml:1: error: Type object expected as the return value of "plugin"; got None (in <ROOT>/test-data/unit/plugins/badreturn.py)
+
+[case testInvalidPluginEntryPointReturnValue2PyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+def f(): pass
+f()
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/badreturn2.py'
+[out]
+tmp/pyproject.toml:1: error: Return value of "plugin" must be a subclass of "mypy.plugin.Plugin" (in <ROOT>/test-data/unit/plugins/badreturn2.py)
+
+[case testAttributeTypeHookPluginPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from typing import Callable
+from m import Signal, DerivedSignal
+s: Signal[Callable[[int], None]] = Signal()
+s(1)
+s('') # E: Argument 1 has incompatible type "str"; expected "int"
+
+ds: DerivedSignal[Callable[[int], None]] = DerivedSignal()
+ds('') # E: Argument 1 has incompatible type "str"; expected "int"
+[file m.py]
+from typing import TypeVar, Generic, Callable
+T = TypeVar('T', bound=Callable[..., None])
+class Signal(Generic[T]):
+    __call__: Callable[..., None]  # This type is replaced by the plugin
+
+class DerivedSignal(Signal[T]): ...
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/attrhook.py'
+
+[case testAttributeHookPluginForDynamicClassPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from m import Magic, DerivedMagic
+
+magic = Magic()
+reveal_type(magic.magic_field)  # N: Revealed type is 'builtins.str'
+reveal_type(magic.non_magic_method())  # N: Revealed type is 'builtins.int'
+reveal_type(magic.non_magic_field)  # N: Revealed type is 'builtins.int'
+magic.nonexistent_field  # E: Field does not exist
+reveal_type(magic.fallback_example)  # N: Revealed type is 'Any'
+reveal_type(DerivedMagic().magic_field)  # N: Revealed type is 'builtins.str'
+[file m.py]
+from typing import Any
+class Magic:
+    # Triggers plugin infrastructure:
+    def __getattr__(self, x: Any) -> Any: ...
+    def non_magic_method(self) -> int: ...
+    non_magic_field: int
+
+class DerivedMagic(Magic): ...
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/attrhook2.py'
+
+[case testTypeAnalyzeHookPluginPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from typing import Callable
+from mypy_extensions import DefaultArg
+from m import Signal
+s: Signal[[int, DefaultArg(str, 'x')]] = Signal()
+reveal_type(s) # N: Revealed type is 'm.Signal[def (builtins.int, x: builtins.str =)]'
+s.x # E: "Signal[Callable[[int, str], None]]" has no attribute "x"
+ss: Signal[int, str] # E: Invalid "Signal" type (expected "Signal[[t, ...]]")
+[file m.py]
+from typing import TypeVar, Generic, Callable
+T = TypeVar('T', bound=Callable[..., None])
+class Signal(Generic[T]):
+    __call__: Callable[..., None]
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/type_anal_hook.py'
+[builtins fixtures/dict.pyi]
+
+[case testFunctionPluginHookForClassPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import mod
+from mod import AttrInt
+
+Alias = AttrInt
+AnotherAlias = mod.Attr
+
+class C:
+    x = Alias()
+    y = mod.AttrInt(required=True)
+    z = AnotherAlias(int, required=False)
+
+c = C()
+reveal_type(c.x)  # N: Revealed type is 'Union[builtins.int, None]'
+reveal_type(c.y)  # N: Revealed type is 'builtins.int*'
+reveal_type(c.z)  # N: Revealed type is 'Union[builtins.int*, None]'
+
+[file mod.py]
+from typing import Generic, TypeVar, Type
+T = TypeVar('T')
+
+class Attr(Generic[T]):
+    def __init__(self, tp: Type[T], required: bool = False) -> None:
+        pass
+    def __get__(self, instance: object, owner: type) -> T:
+        pass
+
+class AttrInt(Attr[int]):
+    def __init__(self, required: bool = False) -> None:
+        pass
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/class_callable.py'
+[builtins fixtures/bool.pyi]
+[out]
+
+[case testFunctionPluginHookForReturnedCallablePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from m import decorator1, decorator2
+@decorator1()
+def f() -> None: pass
+@decorator2()
+def g() -> None: pass
+reveal_type(f) # N: Revealed type is 'def (*Any, **Any) -> builtins.str'
+reveal_type(g) # N: Revealed type is 'def (*Any, **Any) -> builtins.int'
+[file m.py]
+from typing import Callable
+def decorator1() -> Callable[..., Callable[..., int]]: pass
+def decorator2() -> Callable[..., Callable[..., int]]: pass
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/named_callable.py'
+
+[case testFunctionMethodContextsHasArgNamesPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import Class, func
+
+reveal_type(Class().method(arg1=1, arg2=2, classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(Class.myclassmethod(arg1=1, arg2=2, classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(Class.mystaticmethod(arg1=1, arg2=2, classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(Class.method(self=Class(), arg1=1, arg2=2, classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(func(arg1=1, arg2=2, classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+
+[file mod.py]
+from typing import Any
+class Class:
+    def method(self, classname: str, arg1: Any, arg2: Any) -> Any:
+        pass
+    @classmethod
+    def myclassmethod(cls, classname: str, arg1: Any, arg2: Any):
+        pass
+    @staticmethod
+    def mystaticmethod(classname: str, arg1: Any, arg2: Any):
+        pass
+def func(classname: str, arg1: Any, arg2: Any) -> Any:
+    pass
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_names.py'
+[builtins fixtures/classmethod.pyi]
+
+[case testFunctionMethodContextsHasArgNamesPositionalsPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import Class, func
+
+reveal_type(Class().method('builtins.str', arg1=1, arg2=2))  # N: Revealed type is 'builtins.str'
+reveal_type(Class.myclassmethod('builtins.str', arg1=1, arg2=2))  # N: Revealed type is 'builtins.str'
+reveal_type(Class.mystaticmethod('builtins.str', arg1=1, arg2=2))  # N: Revealed type is 'builtins.str'
+reveal_type(Class.method(Class(), 'builtins.str', arg1=1, arg2=2))  # N: Revealed type is 'builtins.str'
+reveal_type(func('builtins.str', arg1=1, arg2=2))  # N: Revealed type is 'builtins.str'
+
+[file mod.py]
+from typing import Any
+class Class:
+    def method(self, classname: str, arg1: Any, arg2: Any) -> Any:
+        pass
+    @classmethod
+    def myclassmethod(cls, classname: str, arg1: Any, arg2: Any):
+        pass
+    @staticmethod
+    def mystaticmethod(classname: str, arg1: Any, arg2: Any):
+        pass
+def func(classname: str, arg1: Any, arg2: Any) -> Any:
+    pass
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_names.py'
+[builtins fixtures/classmethod.pyi]
+
+[case testFunctionMethodContextsHasArgNamesInitMethodPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import ClassInit, Outer
+
+reveal_type(ClassInit('builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassInit(classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(Outer.NestedClassInit(classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+[file mod.py]
+from typing import Any
+class ClassInit:
+    def __init__(self, classname: str):
+        pass
+class Outer:
+    class NestedClassInit:
+        def __init__(self, classname: str):
+            pass
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_names.py'
+
+[case testFunctionMethodContextsHasArgNamesUnfilledArgumentsPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import ClassUnfilled, func_unfilled
+
+reveal_type(ClassUnfilled().method(classname='builtins.str', arg1=1))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassUnfilled().method(arg2=1, classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassUnfilled().method('builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(func_unfilled(classname='builtins.str', arg1=1))  # N: Revealed type is 'builtins.str'
+reveal_type(func_unfilled(arg2=1, classname='builtins.str'))  # N: Revealed type is 'builtins.str'
+reveal_type(func_unfilled('builtins.str'))  # N: Revealed type is 'builtins.str'
+
+[file mod.py]
+from typing import Any
+class ClassUnfilled:
+    def method(self, classname: str, arg1: Any = None, arg2: Any = None) -> Any:
+        pass
+def func_unfilled(classname: str, arg1: Any = None, arg2: Any = None) -> Any:
+    pass
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_names.py'
+
+[case testFunctionMethodContextsHasArgNamesStarExpressionsPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import ClassStarExpr, func_star_expr
+
+reveal_type(ClassStarExpr().method(classname='builtins.str', arg1=1))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassStarExpr().method('builtins.str', arg1=1))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassStarExpr().method('builtins.str', arg1=1, arg2=1))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassStarExpr().method('builtins.str', 2, 3, 4, arg1=1, arg2=1))  # N: Revealed type is 'builtins.str'
+reveal_type(func_star_expr(classname='builtins.str', arg1=1))  # N: Revealed type is 'builtins.str'
+reveal_type(func_star_expr('builtins.str', arg1=1))  # N: Revealed type is 'builtins.str'
+reveal_type(func_star_expr('builtins.str', 2, 3, 4, arg1=1, arg2=2))  # N: Revealed type is 'builtins.str'
+
+[file mod.py]
+from typing import Any
+class ClassStarExpr:
+    def method(self, classname: str, *args, **kwargs) -> Any:
+        pass
+def func_star_expr(classname: str, *args, **kwargs) -> Any:
+    pass
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_names.py'
+[builtins fixtures/dict.pyi]
+
+[case testFunctionMethodContextArgNamesForInheritedMethodsPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import ClassChild
+
+reveal_type(ClassChild().method(classname='builtins.str', arg1=1, arg2=1))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassChild().method(arg1=1, classname='builtins.str', arg2=1))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassChild().method('builtins.str', arg1=1, arg2=1))  # N: Revealed type is 'builtins.str'
+reveal_type(ClassChild.myclassmethod('builtins.str'))  # N: Revealed type is 'builtins.str'
+[file mod.py]
+from typing import Any
+class Base:
+    def method(self, classname: str, arg1: Any, arg2: Any) -> Any:
+        pass
+    @classmethod
+    def myclassmethod(cls, classname: str) -> Any:
+        pass
+class ClassChild(Base):
+    pass
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_names.py'
+[builtins fixtures/classmethod.pyi]
+
+[case testMethodSignatureHookPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from typing import Iterator
+
+class Foo:
+    # Test that method signature hooks are applied in various cases: explicit method calls, and
+    # implicit dunder method calls through language syntax.
+    # The plugin's method signature hook should turn all str occurrences into int.
+    def __init__(self) -> None: ...
+    def __getitem__(self, index: str) -> str: ...
+    def __setitem__(self, index: str, value: str) -> None: ...
+    def __iter__(self) -> Iterator[str]: ...
+    def __next__(self) -> str: ...
+    def __call__(self, *args: str) -> str: ...
+    def m(self, arg: str) -> str: ...
+
+foo = Foo()
+reveal_type(foo.m(2)) # N: Revealed type is 'builtins.int'
+reveal_type(foo[3]) # N: Revealed type is 'builtins.int'
+reveal_type(foo(4, 5, 6)) # N: Revealed type is 'builtins.int'
+foo[4] = 5
+for x in foo:
+    reveal_type(x) # N: Revealed type is 'builtins.int*'
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/method_sig_hook.py'
+[builtins fixtures/tuple.pyi]
+
+[case testMethodSignatureHookNamesFullyQualifiedPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mypy_extensions import TypedDict
+from typing import NamedTuple
+
+class FullyQualifiedTestClass:
+    @classmethod
+    def class_method(self) -> str: ...
+    def instance_method(self) -> str: ...
+
+class FullyQualifiedTestTypedDict(TypedDict):
+    foo: str
+
+FullyQualifiedTestNamedTuple = NamedTuple('FullyQualifiedTestNamedTuple', [('foo', str)])
+
+# Check the return types to ensure that the method signature hook is called in each case
+reveal_type(FullyQualifiedTestClass.class_method()) # N: Revealed type is 'builtins.int'
+reveal_type(FullyQualifiedTestClass().instance_method()) # N: Revealed type is 'builtins.int'
+reveal_type(FullyQualifiedTestNamedTuple('')._asdict()) # N: Revealed type is 'builtins.int'
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/fully_qualified_test_hook.py'
+[builtins fixtures/classmethod.pyi]
+
+[case testDynamicClassPluginPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import declarative_base, Column, Instr
+
+Base = declarative_base()
+
+class Model(Base):
+    x: Column[int]
+class Other:
+    x: Column[int]
+
+reveal_type(Model().x)  # N: Revealed type is 'mod.Instr[builtins.int]'
+reveal_type(Other().x)  # N: Revealed type is 'mod.Column[builtins.int]'
+[file mod.py]
+from typing import Generic, TypeVar
+def declarative_base(): ...
+
+T = TypeVar('T')
+
+class Column(Generic[T]): ...
+class Instr(Generic[T]): ...
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/dyn_class.py'
+
+[case testDynamicClassPluginNegativesPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import declarative_base, Column, Instr, non_declarative_base
+
+Bad1 = non_declarative_base()
+Bad2 = Bad3 = declarative_base()
+
+class C1(Bad1): ...  # E: Variable "__main__.Bad1" is not valid as a type \
+                     # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \
+                     # E: Invalid base class "Bad1"
+class C2(Bad2): ...  # E: Variable "__main__.Bad2" is not valid as a type \
+                     # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \
+                     # E: Invalid base class "Bad2"
+class C3(Bad3): ...  # E: Variable "__main__.Bad3" is not valid as a type \
+                     # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \
+                     # E: Invalid base class "Bad3"
+[file mod.py]
+from typing import Generic, TypeVar
+def declarative_base(): ...
+def non_declarative_base(): ...
+
+T = TypeVar('T')
+
+class Column(Generic[T]): ...
+class Instr(Generic[T]): ...
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/dyn_class.py'
+
+[case testDynamicClassHookFromClassMethodPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+
+from mod import QuerySet, Manager
+
+MyManager = Manager.from_queryset(QuerySet)
+
+reveal_type(MyManager())  # N: Revealed type is '__main__.MyManager'
+reveal_type(MyManager().attr)  # N: Revealed type is 'builtins.str'
+
+def func(manager: MyManager) -> None:
+    reveal_type(manager)   # N: Revealed type is '__main__.MyManager'
+    reveal_type(manager.attr)   # N: Revealed type is 'builtins.str'
+
+func(MyManager())
+
+[file mod.py]
+from typing import Generic, TypeVar, Type
+class QuerySet:
+    attr: str
+class Manager:
+    @classmethod
+    def from_queryset(cls, queryset_cls: Type[QuerySet]): ...
+
+[builtins fixtures/classmethod.pyi]
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/dyn_class_from_method.py'
+
+[case testBaseClassPluginHookWorksIncrementalPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+
+[file a.py]
+from base import Base
+class C(Base): ...
+
+[file a.py.2]
+from base import Base
+class C(Base): ...
+reveal_type(C().__magic__)
+Base.__magic__
+
+[file base.py]
+from lib import declarative_base
+Base = declarative_base()
+
+[file lib.py]
+from typing import Any
+def declarative_base() -> Any: ...
+
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 3.6
+plugins = '<ROOT>/test-data/unit/plugins/common_api_incremental.py'
+[out]
+[out2]
+tmp/a.py:3: note: Revealed type is 'builtins.str'
+tmp/a.py:4: error: "Type[Base]" has no attribute "__magic__"
+
+[case testArgKindsMethodPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+class Class:
+    def method(self, *args, **kwargs):
+        pass
+
+Class().method(1, *[2], **{'a': 1})  # E: [[0, 2], [4]]
+[builtins fixtures/dict.pyi]
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_kinds.py'
+
+[case testArgKindsFunctionPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+def func(*args, **kwargs):
+    pass
+
+func(1, 2, [3, 4], *[5, 6, 7], **{'a': 1})  # E: [[0, 0, 0, 2], [4]]
+[builtins fixtures/dict.pyi]
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/arg_kinds.py'
+
+[case testHookCallableInstancePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from typing import Generic, TypeVar
+T = TypeVar("T")
+class Class(Generic[T]):
+    def __init__(self, one: T): ...
+    def __call__(self, two: T) -> int: ...
+reveal_type(Class("hi")("there"))  # N: Revealed type is 'builtins.str*'
+instance = Class(3.14)
+reveal_type(instance(2))  # N: Revealed type is 'builtins.float*'
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/callable_instance.py'
+
+[case testGetMethodHooksOnUnionsPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml --no-strict-optional
+from typing import Union
+
+class Foo:
+    def meth(self, x: str) -> str: ...
+class Bar:
+    def meth(self, x: int) -> float: ...
+class Other:
+    meth: int
+
+x: Union[Foo, Bar, Other]
+if isinstance(x.meth, int):
+    reveal_type(x.meth)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(x.meth(int()))  # N: Revealed type is 'builtins.int'
+
+[builtins fixtures/isinstancelist.pyi]
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/union_method.py'
+
+[case testGetMethodHooksOnUnionsStrictOptionalPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml --strict-optional
+from typing import Union
+
+class Foo:
+    def meth(self, x: str) -> str: ...
+class Bar:
+    def meth(self, x: int) -> float: ...
+class Other:
+    meth: int
+
+x: Union[Foo, Bar, Other]
+if isinstance(x.meth, int):
+    reveal_type(x.meth)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(x.meth(int()))  # N: Revealed type is 'builtins.int'
+
+[builtins fixtures/isinstancelist.pyi]
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/union_method.py'
+
+[case testGetMethodHooksOnUnionsSpecialPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from typing import Union
+
+class Foo:
+    def __getitem__(self, x: str) -> str: ...
+class Bar:
+    def __getitem__(self, x: int) -> float: ...
+
+x: Union[Foo, Bar]
+reveal_type(x[int()])  # N: Revealed type is 'builtins.int'
+
+[builtins fixtures/isinstancelist.pyi]
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/union_method.py'
+
+[case testPluginDependenciesPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+
+# The top level file here doesn't do anything, but the plugin should add
+# a dependency on err that will cause err to be processed and an error reported.
+
+[file err.py]
+1 + 'lol'  # E: Unsupported operand types for + ("int" and "str")
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/depshook.py'
+
+[case testCustomizeMroTrivialPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+class A: pass
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/customize_mro.py'
+
+[case testDescriptorMethodsPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+
+class Desc:
+    def __get__(self, obj, cls):
+        pass
+
+    def __set__(self, obj, val):
+        pass
+
+class Cls:
+    attr = Desc()
+
+reveal_type(Cls().attr)  # N: Revealed type is 'builtins.int'
+reveal_type(Cls.attr)  # N: Revealed type is 'builtins.str'
+
+Cls().attr = 3
+Cls().attr = "foo"  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/descriptor.py'
+
+[case testFunctionSigPluginFilePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+
+def dynamic_signature(arg1: str) -> str: ...
+reveal_type(dynamic_signature(1))  # N: Revealed type is 'builtins.int'
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/function_sig_hook.py'
diff --git a/test-data/unit/check-flags.pyproject.test b/test-data/unit/check-flags.pyproject.test
new file mode 100644
index 000000000000..532ff99e248a
--- /dev/null
+++ b/test-data/unit/check-flags.pyproject.test
@@ -0,0 +1,278 @@
+[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-incomplete]
+disallow_incomplete_defs = 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-optional]
+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-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-optional]
+strict_optional = true
+
+[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-optional]
+strict_optional = true
+[builtins fixtures/list.pyi]
+
+[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-optional]
+strict_optional = true
+
+[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 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 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 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 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-b]
+strict_equality = false
+[builtins fixtures/bool.pyi]
+
+[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-other_module_2]
+implicit_reexport = false
+[out]
+main:2: error: Module 'other_module_2' has no attribute 'a'
+
+[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-m]
+disallow_subclassing_any = 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-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-m]
+no_implicit_optional = false
+
+[case testDisableErrorCodePyprojectTOML]
+# flags: --disable-error-code attr-defined
+x = 'should be fine'
+x.trim()
diff --git a/test-data/unit/check-incremental.pyproject.test b/test-data/unit/check-incremental.pyproject.test
new file mode 100644
index 000000000000..745a1fdeaa8d
--- /dev/null
+++ b/test-data/unit/check-incremental.pyproject.test
@@ -0,0 +1,306 @@
+-- Checks for incremental mode (see testcheck.py).
+-- Each test is run at least twice, once with a cold cache, once with a warm cache.
+-- Before the tests are run again, in step N any *.py.N files are copied to
+-- *.py.  There are at least two runs; more as long as there are *.py.N files.
+--
+-- You can add an empty section like `[delete mod.py.2]` to delete `mod.py`
+-- before the second run.
+--
+-- Errors expected in the first run should be in the `[out1]` section, and
+-- errors expected in the second run should be in the `[out2]` section, and so on.
+-- If a section is omitted, it is expected there are no errors on that run.
+-- The number of runs is determined by the highest N in all [outN] sections, but
+-- there are always at least two runs. (Note that [out] is equivalent to [out1].)
+--
+-- The list of modules to be checked can be specified using
+-- # cmd: mypy -m mod1 mod2 mod3
+-- To check a different list on the second run, use
+-- # cmd2: mypy -m mod1 mod3
+-- (and cmd3 for the third run, and so on).
+--
+-- Extra command line flags may be specified using
+-- # flags: --some-flag
+-- If the second run requires different flags, those can be specified using
+-- # flags2: --another-flag
+-- (and flags3 for the third run, and so on).
+--
+-- Incremental tests involving plugins that get updated are also supported.
+-- All plugin files that are updated *must* end in '_plugin', so they will
+-- be unloaded from 'sys.modules' between incremental steps.
+--
+-- Any files that we expect to be rechecked should be annotated in the [rechecked]
+-- annotation, and any files expect to be stale (aka have a modified interface)
+-- should be annotated in the [stale] annotation. Note that a file that ends up
+-- producing an error has its caches deleted and is marked stale automatically.
+-- Such files do not need to be included in [stale ...] list.
+--
+-- The test suite will automatically assume that __main__ is stale and rechecked in
+-- all cases so we can avoid constantly having to annotate it. The list of
+-- rechecked/stale files can be in any arbitrary order, or can be left empty
+-- if no files should be rechecked/stale.
+--
+-- There are additional incremental mode test cases in check-serialize.test.
+
+[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 testIncrementalPerFileFlagsPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+[file a.py]
+pass
+[file pyproject.toml]
+\[tool.mypy]
+warn_no_return = false
+\[tool.mypy-a]
+warn_no_return = true
+[rechecked]
+
+[case testRegularUsesFgCachePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+[file a.py]
+x = 0
+[file pyproject.toml]
+\[tool.mypy]
+cache_fine_grained = true
+[file pyproject.toml.2]
+\[tool.mypy]
+cache_fine_grained = false
+-- Nothing should get rechecked
+[rechecked]
+[stale]
+
+[case testFgCacheNeedsFgCachePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+[file a.py]
+x = 0
+[file pyproject.toml]
+\[tool.mypy]
+cache_fine_grained = false
+[file pyproject.toml.2]
+\[tool.mypy]
+cache_fine_grained = true
+[rechecked a, builtins, typing]
+[stale a, builtins, typing]
+[builtins fixtures/tuple.pyi]
+
+[case testFollowImportSkipNotInvalidatedOnAddedStubOnFollowForStubsPyprojectTOML]
+# flags: --follow-imports=skip --ignore-missing-imports --config-file=tmp/pyproject.toml
+# cmd: mypy -m main
+[file main.py]
+import other
+[file other.pyi.2]
+x = 1
+[file pyproject.toml]
+\[tool.mypy]
+follow_imports_for_stubs = true
+[stale]
+[rechecked]
+
+[case testChangedPluginsInvalidateCachePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+[file a.py]
+from b import x
+y: int = x
+
+[file a.py.2]
+from b import x
+y: int = x
+touch = 1
+
+[file b.py]
+class C: ...
+def f() -> C: ...
+x = f()
+
+[file basic_plugin.py]
+from mypy.plugin import Plugin
+
+class MyPlugin(Plugin):
+    def get_function_hook(self, fullname):
+        if fullname.endswith('.f'):
+            return my_hook
+        assert fullname is not None
+        return None
+
+def my_hook(ctx):
+    return ctx.api.named_generic_type('builtins.int', [])
+
+def plugin(version):
+    return MyPlugin
+
+[file basic_plugin.py.2]
+from mypy.plugin import Plugin
+
+class MyPlugin(Plugin):
+    def get_function_hook(self, fullname):
+        if fullname.endswith('.f'):
+            return my_hook
+        assert fullname is not None
+        return None
+
+def my_hook(ctx):
+    return ctx.api.named_generic_type('builtins.str', [])
+
+def plugin(version):
+    return MyPlugin
+[file pyproject.toml]
+\[tool.mypy]
+plugins = "basic_plugin.py"
+[out]
+[out2]
+tmp/a.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[case testChangedPluginsInvalidateCache2PyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+[file a.py]
+from b import x
+y: int = x
+
+[file a.py.2]
+from b import x
+y: int = x
+touch = 1
+
+[file b.py]
+class C: ...
+def f() -> C: ...
+x = f()
+
+[file basic_plugin.py]
+from mypy.plugin import Plugin
+from version_plugin import __version__, choice
+
+class MyPlugin(Plugin):
+    def get_function_hook(self, fullname):
+        if fullname.endswith('.f'):
+            return my_hook
+        assert fullname is not None
+        return None
+
+def my_hook(ctx):
+    if choice:
+        return ctx.api.named_generic_type('builtins.int', [])
+    else:
+        return ctx.api.named_generic_type('builtins.str', [])
+
+def plugin(version):
+    return MyPlugin
+
+[file version_plugin.py]
+__version__ = 0.1
+choice = True
+
+[file version_plugin.py.2]
+__version__ = 0.2
+choice = False
+[file pyproject.toml]
+\[tool.mypy]
+plugins = "basic_plugin.py"
+[out]
+[out2]
+tmp/a.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[case testAddedPluginsInvalidateCachePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+[file a.py]
+from b import x
+y: int = x
+
+[file a.py.2]
+from b import x
+y: int = x
+touch = 1
+
+[file b.py]
+def f() -> int: ...
+x = f()
+
+[file basic_plugin.py]
+from mypy.plugin import Plugin
+
+class MyPlugin(Plugin):
+    def get_function_hook(self, fullname):
+        if fullname.endswith('.f'):
+            return my_hook
+        assert fullname is not None
+        return None
+
+def my_hook(ctx):
+    return ctx.api.named_generic_type('builtins.str', [])
+
+def plugin(version):
+    return MyPlugin
+
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 3.6
+[file pyproject.toml.2]
+\[tool.mypy]
+python_version = 3.6
+plugins = "basic_plugin.py"
+[out]
+[out2]
+tmp/a.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[case testRemovedPluginsInvalidateCachePyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+import a
+[file a.py]
+from b import x
+y: str = x
+
+[file a.py.2]
+from b import x
+y: str = x
+touch = 1
+
+[file b.py]
+def f() -> int: ...
+x = f()
+
+[file basic_plugin.py]
+from mypy.plugin import Plugin
+
+class MyPlugin(Plugin):
+    def get_function_hook(self, fullname):
+        if fullname.endswith('.f'):
+            return my_hook
+        assert fullname is not None
+        return None
+
+def my_hook(ctx):
+    return ctx.api.named_generic_type('builtins.str', [])
+
+def plugin(version):
+    return MyPlugin
+
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 3.6
+plugins = "basic_plugin.py"
+[file pyproject.toml.2]
+\[tool.mypy]
+python_version = 3.6
+[out]
+[out2]
+tmp/a.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str")
diff --git a/test-data/unit/check-modules-case.pyproject.test b/test-data/unit/check-modules-case.pyproject.test
new file mode 100644
index 000000000000..210b56bfa119
--- /dev/null
+++ b/test-data/unit/check-modules-case.pyproject.test
@@ -0,0 +1,56 @@
+-- Type checker test cases dealing with modules and imports on case-insensitive filesystems.
+
+[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 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 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 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.pyproject.test b/test-data/unit/check-modules.pyproject.test
new file mode 100644
index 000000000000..633b68b6bf75
--- /dev/null
+++ b/test-data/unit/check-modules.pyproject.test
@@ -0,0 +1,92 @@
+-- Type checker test cases dealing with modules and imports.
+-- Towards the end there are tests for PEP 420 (namespace packages, i.e. __init__.py-less packages).
+
+[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-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 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 testClassicPackageIgnoresEarlierNamespacePackagePyprojectTOML]
+# flags: --namespace-packages --config-file tmp/pyproject.toml
+from foo.bar import y
+reveal_type(y)  # N: Revealed type is 'builtins.int'
+[file xx/foo/bar.py]
+x = ''
+[file yy/foo/bar.py]
+y = 0
+[file yy/foo/__init__.py]
+[file pyproject.toml]
+\[tool.mypy]
+mypy_path = "tmp/xx,tmp/yy"
+
+[case testNamespacePackagePickFirstOnMypyPathPyprojectTOML]
+# 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 testNamespacePackageInsideClassicPackagePyprojectTOML]
+# flags: --namespace-packages --config-file tmp/pyproject.toml
+from foo.bar.baz import x
+reveal_type(x)  # N: Revealed type is 'builtins.int'
+[file xx/foo/bar/baz.py]
+x = ''
+[file yy/foo/bar/baz.py]
+x = 0
+[file yy/foo/__init__.py]
+[file pyproject.toml]
+\[tool.mypy]
+mypy_path = "tmp/xx,tmp/yy"
+
+[case testClassicPackageInsideNamespacePackagePyprojectTOML]
+# 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-optional.pyproject.test b/test-data/unit/check-optional.pyproject.test
new file mode 100644
index 000000000000..eed065332847
--- /dev/null
+++ b/test-data/unit/check-optional.pyproject.test
@@ -0,0 +1,26 @@
+-- Tests for strict Optional behavior
+
+[case testStrictOptionalCovarianceCrossModulePyprojectTOMLPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from a import asdf
+x = ["lol"]
+asdf(x)
+
+[file a.py]
+from typing import List, Optional
+
+def asdf(x: List[Optional[str]]) -> None:
+    pass
+
+x = ["lol"]
+asdf(x)
+
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.mypy-a]
+strict_optional = false
+[out]
+main:4: error: Argument 1 to "asdf" has incompatible type "List[str]"; expected "List[Optional[str]]"
+main:4: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
+main:4: note: Consider using "Sequence" instead, which is covariant
+[builtins fixtures/list.pyi]
diff --git a/test-data/unit/cmdline.pyproject.test b/test-data/unit/cmdline.pyproject.test
new file mode 100644
index 000000000000..71e82e069f19
--- /dev/null
+++ b/test-data/unit/cmdline.pyproject.test
@@ -0,0 +1,695 @@
+-- Tests for command line parsing
+-- ------------------------------
+--
+-- The initial line specifies the command line, in the format
+--
+--   # cmd: mypy <options>
+--
+-- Note that # flags: --some-flag IS NOT SUPPORTED.
+-- Use # cmd: mypy --some-flag ...
+--
+-- '== Return code: <value>' 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
+-- ----------------------------------------
+
+-- Tests mirrored off of cmdline.test
+
+[case testConfigFilePyprojectTOML]
+# cmd: mypy --config-file pyproject.toml main.py
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 2.7
+[file main.py]
+def f():
+    try:
+        1/0
+    except ZeroDivisionError, err:
+        print err
+
+[case testErrorContextConfigPyprojectTOML]
+# cmd: mypy main.py
+[file pyproject.toml]
+\[tool.mypy]
+show_error_context = true
+[file main.py]
+def f() -> None:
+  0 + ""
+[out]
+main.py: note: In function "f":
+main.py:2: error: Unsupported operand types for + ("int" and "str")
+
+[case testNoConfigFilePyprojectTOML]
+# cmd: mypy main.py --config-file=
+[file pyproject.toml]
+\[tool.mypy]
+warn_unused_ignores = true
+[file main.py]
+# type: ignore
+
+[case testPerFileConfigSectionPyprojectTOML]
+# cmd: mypy x.py y.py z.py
+[file pyproject.toml]
+\[tool.mypy]
+disallow_untyped_defs = true
+\[tool.mypy-y]
+disallow_untyped_defs = false
+\[tool.mypy-z]
+disallow_untyped_calls = true
+[file x.py]
+def f(a):
+    pass
+def g(a: int) -> int:
+    return f(a)
+[file y.py]
+def f(a):
+    pass
+def g(a: int) -> int:
+    return f(a)
+[file z.py]
+def f(a):
+    pass
+def g(a: int) -> int:
+    return f(a)
+[out]
+z.py:1: error: Function is missing a type annotation
+z.py:4: error: Call to untyped function "f" in typed context
+x.py:1: error: Function is missing a type annotation
+
+[case testPerFileConfigSectionMultipleMatchesDisallowedPyprojectTOML]
+# cmd: mypy xx.py xy.py yx.py yy.py
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.'mypy-*x*']
+disallow_untyped_defs = true
+\[tool.'mypy-*y*']
+disallow_untyped_calls = true
+[file xx.py]
+def f(a): pass
+def g(a: int) -> int: return f(a)
+[file xy.py]
+def f(a): pass
+def g(a: int) -> int: return f(a)
+[file yx.py]
+def f(a): pass
+def g(a: int) -> int: return f(a)
+[file yy.py]
+def f(a): pass
+def g(a: int) -> int: return f(a)
+[out]
+pyproject.toml: [mypy-*x*]: Patterns must be fully-qualified module names, optionally with '*' in some components (e.g spam.*.eggs.*)
+pyproject.toml: [mypy-*y*]: Patterns must be fully-qualified module names, optionally with '*' in some components (e.g spam.*.eggs.*)
+== Return code: 0
+
+[case testMultipleGlobConfigSectionPyprojectTOML]
+# cmd: mypy x.py y.py z.py
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.'mypy-x.*,z.*']
+disallow_untyped_defs = true
+[file x.py]
+def f(a): pass
+[file y.py]
+def f(a): pass
+[file z.py]
+def f(a): pass
+[out]
+z.py:1: error: Function is missing a type annotation
+x.py:1: error: Function is missing a type annotation
+
+[case testConfigNoErrorNoSectionPyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+[out]
+
+[case testConfigErrorUnknownFlagPyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+bad = 0
+[out]
+pyproject.toml: [mypy]: Unrecognized option: bad = 0
+== Return code: 0
+
+[case testConfigErrorBadFlagPyprojectTOML]
+# cmd: mypy a.py
+[file pyproject.toml]
+\[tool.mypy]
+disallow-untyped-defs = true
+[file a.py]
+def f():
+    pass
+[out]
+pyproject.toml: [mypy]: Unrecognized option: disallow-untyped-defs = True
+== Return code: 0
+
+[case testConfigErrorBadBooleanPyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+ignore_missing_imports = "nah"
+[out]
+pyproject.toml: [mypy]: ignore_missing_imports: Not a boolean: nah
+== Return code: 0
+
+[case testConfigErrorNotPerFilePyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.'mypy-*']
+python_version = 3.4
+[out]
+pyproject.toml: [mypy-*]: Per-module sections should only specify per-module flags (python_version)
+== Return code: 0
+
+[case testConfigMypyPathPyprojectTOML]
+# cmd: mypy file.py
+[file pyproject.toml]
+\[tool.mypy]
+mypy_path = "foo:bar,baz"
+[file foo/foo.pyi]
+def foo(x: int) -> str: ...
+[file bar/bar.pyi]
+def bar(x: str) -> list: ...
+[file baz/baz.pyi]
+def baz(x: list) -> dict: ...
+[file file.py]
+import no_stubs
+from foo import foo
+from bar import bar
+from baz import baz
+baz(bar(foo(42)))
+baz(bar(foo('oof')))
+[out]
+file.py:1: error: Cannot find implementation or library stub for module named "no_stubs"
+file.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
+file.py:6: error: Argument 1 to "foo" has incompatible type "str"; expected "int"
+
+[case testIgnoreErrorsConfigPyprojectTOML]
+# cmd: mypy x.py y.py
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.mypy-x]
+ignore_errors = true
+[file x.py]
+"" + 0
+[file y.py]
+"" + 0
+[out]
+y.py:1: error: Unsupported operand types for + ("str" and "int")
+
+[case testConfigFollowImportsNormalPyprojectTOML]
+# cmd: mypy main.py
+[file main.py]
+from a import x
+x + 0
+x + ''  # E
+import a
+a.x + 0
+a.x + ''  # E
+a.y  # E
+a + 0  # E
+[file pyproject.toml]
+\[tool.mypy]
+follow_imports = "normal"
+[file a.py]
+x = 0
+x += ''  # Error reported here
+[out]
+a.py:2: error: Unsupported operand types for + ("int" and "str")
+main.py:3: error: Unsupported operand types for + ("int" and "str")
+main.py:6: error: Unsupported operand types for + ("int" and "str")
+main.py:7: error: Module has no attribute "y"
+main.py:8: error: Unsupported operand types for + (Module and "int")
+
+[case testConfigFollowImportsSilentPyprojectTOML]
+# cmd: mypy main.py
+[file main.py]
+from a import x
+x + ''
+import a
+a.x + ''
+a.y
+a + 0
+[file pyproject.toml]
+\[tool.mypy]
+follow_imports = "silent"
+[file a.py]
+x = 0
+x += ''  # No error reported
+[out]
+main.py:2: error: Unsupported operand types for + ("int" and "str")
+main.py:4: error: Unsupported operand types for + ("int" and "str")
+main.py:5: error: Module has no attribute "y"
+main.py:6: error: Unsupported operand types for + (Module and "int")
+
+[case testConfigFollowImportsSkipPyprojectTOML]
+# cmd: mypy main.py
+[file main.py]
+from a import x
+reveal_type(x)  # Expect Any
+import a
+reveal_type(a.x)  # Expect Any
+[file pyproject.toml]
+\[tool.mypy]
+follow_imports = "skip"
+[file a.py]
+/  # No error reported
+[out]
+main.py:2: note: Revealed type is 'Any'
+main.py:4: note: Revealed type is 'Any'
+
+[case testConfigFollowImportsErrorPyprojectTOML]
+# cmd: mypy main.py
+[file main.py]
+from a import x
+reveal_type(x)  # Expect Any
+import a  # Error reported here
+reveal_type(a.x)  # Expect Any
+[file pyproject.toml]
+\[tool.mypy]
+follow_imports = "error"
+[file a.py]
+/  # No error reported
+[out]
+main.py:1: error: Import of 'a' ignored
+main.py:1: note: (Using --follow-imports=error, module not passed on command line)
+main.py:2: note: Revealed type is 'Any'
+main.py:4: note: Revealed type is 'Any'
+
+[case testConfigFollowImportsSelectivePyprojectTOML]
+# cmd: mypy main.py
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.mypy-normal]
+follow_imports = "normal"
+\[tool.mypy-silent]
+follow_imports = "silent"
+\[tool.mypy-skip]
+follow_imports = "skip"
+\[tool.mypy-error]
+follow_imports = "error"
+[file main.py]
+import normal
+import silent
+import skip
+import error
+reveal_type(normal.x)
+reveal_type(silent.x)
+reveal_type(skip)
+reveal_type(error)
+[file normal.py]
+x = 0
+x += ''
+[file silent.py]
+x = 0
+x += ''
+[file skip.py]
+bla bla
+[file error.py]
+bla bla
+[out]
+normal.py:2: error: Unsupported operand types for + ("int" and "str")
+main.py:4: error: Import of 'error' ignored
+main.py:4: note: (Using --follow-imports=error, module not passed on command line)
+main.py:5: note: Revealed type is 'builtins.int'
+main.py:6: note: Revealed type is 'builtins.int'
+main.py:7: note: Revealed type is 'Any'
+main.py:8: note: Revealed type is 'Any'
+
+[case testConfigFollowImportsInvalidPyprojectTOML]
+# cmd: mypy main.py
+[file pyproject.toml]
+\[tool.mypy]
+follow_imports = true
+[file main.py]
+[out]
+pyproject.toml: [mypy]: follow_imports: invalid choice 'True' (choose from 'normal', 'silent', 'skip', 'error')
+== Return code: 0
+
+[case testConfigSilentMissingImportsOffPyprojectTOML]
+# cmd: mypy main.py
+[file main.py]
+import missing  # Expect error here
+reveal_type(missing.x)  # Expect Any
+[file pyproject.toml]
+\[tool.mypy]
+ignore_missing_imports = false
+[out]
+main.py:1: error: Cannot find implementation or library stub for module named "missing"
+main.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
+main.py:2: note: Revealed type is 'Any'
+
+[case testConfigSilentMissingImportsOnPyprojectTOML]
+# cmd: mypy main.py
+[file main.py]
+import missing  # No error here
+reveal_type(missing.x)  # Expect Any
+[file pyproject.toml]
+\[tool.mypy]
+ignore_missing_imports = true
+[out]
+main.py:2: note: Revealed type is 'Any'
+
+[case testConfigNoErrorForUnknownXFlagInSubsectionPyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.mypy-foo]
+x_bad = 0
+[out]
+
+[case testPythonVersionTooOld10PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 1.0
+[out]
+pyproject.toml: [mypy]: python_version: Python major version '1' out of range (must be 2 or 3)
+== Return code: 0
+
+[case testPythonVersionTooOld26PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 2.6
+[out]
+pyproject.toml: [mypy]: python_version: Python 2.6 is not supported (must be 2.7)
+== Return code: 0
+
+[case testPythonVersionTooOld33PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 3.3
+[out]
+pyproject.toml: [mypy]: python_version: Python 3.3 is not supported (must be 3.4 or higher)
+== Return code: 0
+
+[case testPythonVersionTooNew28PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 2.8
+[out]
+pyproject.toml: [mypy]: python_version: Python 2.8 is not supported (must be 2.7)
+== Return code: 0
+
+[case testPythonVersionTooNew40PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 4.0
+[out]
+pyproject.toml: [mypy]: python_version: Python major version '4' out of range (must be 2 or 3)
+== Return code: 0
+
+[case testPythonVersionAccepted27PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 2.7
+[out]
+
+[case testPythonVersionAccepted34PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 3.4
+[out]
+
+[case testPythonVersionAccepted36PyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 3.6
+[out]
+
+[case testDisallowAnyGenericsBuiltinCollectionsPyprojectTOML]
+# cmd: mypy m.py
+[file pyproject.toml]
+\[tool.mypy]
+python_version = 3.6
+\[tool.mypy-m]
+disallow_any_generics = true
+
+[file m.py]
+s = tuple([1, 2, 3])  # no error
+
+def f(t: tuple) -> None: pass
+def g() -> list: pass
+def h(s: dict) -> None: pass
+def i(s: set) -> None: pass
+def j(s: frozenset) -> None: pass
+[out]
+m.py:3: error: Implicit generic "Any". Use "typing.Tuple" and specify generic parameters
+m.py:4: error: Implicit generic "Any". Use "typing.List" and specify generic parameters
+m.py:5: error: Implicit generic "Any". Use "typing.Dict" and specify generic parameters
+m.py:6: error: Implicit generic "Any". Use "typing.Set" and specify generic parameters
+m.py:7: error: Implicit generic "Any". Use "typing.FrozenSet" and specify generic parameters
+
+[case testDisallowAnyGenericsTypingCollectionsPyprojectTOML]
+# cmd: mypy m.py
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.mypy-m]
+disallow_any_generics = true
+
+[file m.py]
+from typing import Tuple, List, Dict, Set, FrozenSet
+
+def f(t: Tuple) -> None: pass
+def g() -> List: pass
+def h(s: Dict) -> None: pass
+def i(s: Set) -> None: pass
+def j(s: FrozenSet) -> None: pass
+[out]
+m.py:3: error: Missing type parameters for generic type "Tuple"
+m.py:4: error: Missing type parameters for generic type "List"
+m.py:5: error: Missing type parameters for generic type "Dict"
+m.py:6: error: Missing type parameters for generic type "Set"
+m.py:7: error: Missing type parameters for generic type "FrozenSet"
+
+[case testSectionInheritancePyprojectTOML]
+# cmd: mypy a
+[file a/__init__.py]
+0()
+[file a/foo.py]
+0()
+[file a/b/__init__.py]
+[file a/b/c/__init__.py]
+0()
+[file a/b/c/d/__init__.py]
+[file a/b/c/d/e/__init__.py]
+from typing import List
+def g(x: List) -> None: pass
+g(None)
+[file pyproject.toml]
+\[tool.mypy]
+allow_any_generics = true
+\[tool.'mypy-a.*']
+ignore_errors = true
+\[tool.'mypy-a.b.*']
+disallow_any_generics = true
+ignore_errors = true
+\[tool.'mypy-a.b.c.*']
+ignore_errors = true
+\[tool.'mypy-a.b.c.d.*']
+ignore_errors = true
+\[tool.'mypy-a.b.c.d.e.*']
+ignore_errors = true
+strict_optional = true
+\[tool.'mypy-a.b.c.d.e']
+ignore_errors = false
+[out]
+a/b/c/d/e/__init__.py:2: error: Missing type parameters for generic type "List"
+a/b/c/d/e/__init__.py:3: error: Argument 1 to "g" has incompatible type "None"; expected "List[Any]"
+
+[case testDisallowUntypedDefsAndGenericsPyprojectTOML]
+# cmd: mypy a.py
+[file pyproject.toml]
+\[tool.mypy]
+disallow_untyped_defs = true
+disallow_any_generics = true
+[file a.py]
+def get_tasks(self):
+    return 'whatever'
+[out]
+a.py:1: error: Function is missing a return type annotation
+
+[case testSrcPEP420PackagesPyprojectTOML]
+# cmd: mypy -p anamespace --namespace-packages
+[file pyproject.toml]
+\[tool.mypy]
+mypy_path = "src"
+[file src/setup.cfg]
+[file src/anamespace/foo/__init__.py]
+[file src/anamespace/foo/bar.py]
+def bar(a: int, b: int) -> str:
+    return a + b
+[out]
+src/anamespace/foo/bar.py:2: error: Incompatible return value type (got "int", expected "str")
+
+[case testFollowImportStubs1PyprojectTOML]
+# cmd: mypy main.py
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.'mypy-math.*']
+follow_imports = "error"
+follow_imports_for_stubs = true
+[file main.py]
+import math
+math.frobnicate()
+[out]
+main.py:1: error: Import of 'math' ignored
+main.py:1: note: (Using --follow-imports=error, module not passed on command line)
+
+[case testFollowImportStubs2PyprojectTOML]
+# cmd: mypy main.py
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.'mypy-math.*']
+follow_imports = "skip"
+follow_imports_for_stubs = true
+[file main.py]
+import math
+math.frobnicate()
+
+[case testConfigWarnUnusedSection1PyprojectTOML]
+# cmd: mypy foo.py quux.py spam/eggs.py
+[file pyproject.toml]
+\[tool.mypy]
+warn_unused_configs = true
+incremental = false
+\[tool.mypy-bar]
+\[tool.mypy-foo]
+\[tool.'mypy-baz.*']
+\[tool.'mypy-quux.*']
+\[tool.'mypy-spam.*']
+\[tool.'mypy-spam.eggs']
+\[tool.'mypy-emarg.*']
+\[tool.'mypy-emarg.hatch']
+# Currently we don't treat an unstructured pattern like a.*.b as unused
+# if it matches another section (like a.x.b). This would be reasonable
+# to change. '
+\[tool.'mypy-a.*.b']
+\[tool.'mypy-a.*.c']
+\[tool.'mypy-a.x.b']
+[file foo.py]
+[file quux.py]
+[file spam/__init__.py]
+[file spam/eggs.py]
+[out]
+Warning: unused section(s) in pyproject.toml: [mypy-bar], [mypy-baz.*], [mypy-emarg.*], [mypy-emarg.hatch], [mypy-a.*.c], [mypy-a.x.b]
+== Return code: 0
+
+[case testConfigUnstructuredGlobPyprojectTOML]
+# cmd: mypy emarg foo
+[file pyproject.toml]
+\[tool.mypy]
+ignore_errors = true
+\[tool.'mypy-*.lol']
+ignore_errors = false
+\[tool.'mypy-emarg.*']
+ignore_errors = false
+\[tool.'mypy-emarg.*.villip.*']
+ignore_errors = true
+\[tool.'mypy-emarg.hatch.villip.mankangulisk']
+ignore_errors = false
+[file emarg/__init__.py]
+[file emarg/foo.py]
+fail
+[file emarg/villip.py]
+fail
+[file emarg/hatch/__init__.py]
+[file emarg/hatch/villip/__init__.py]
+[file emarg/hatch/villip/nus.py]
+fail
+[file emarg/hatch/villip/mankangulisk.py]
+fail
+[file foo/__init__.py]
+[file foo/lol.py]
+fail
+[out]
+foo/lol.py:1: error: Name 'fail' is not defined
+emarg/foo.py:1: error: Name 'fail' is not defined
+emarg/hatch/villip/mankangulisk.py:1: error: Name 'fail' is not defined
+
+[case testTomlFilesPyprojectTOML]
+# cmd: mypy
+[file pyproject.toml]
+\[tool.mypy]
+files = [
+    "a.py",
+    "b.py"
+]
+[file a.py]
+fail
+[file b.py]
+fail
+[out]
+b.py:1: error: Name 'fail' is not defined
+a.py:1: error: Name 'fail' is not defined
+
+[case testTomlFilesGlobbingPyprojectTOML]
+# cmd: mypy
+[file pyproject.toml]
+\[tool.mypy]
+files = "**/*.py"
+[file a/b.py]
+fail
+[file c.py]
+fail
+[out]
+a/b.py:1: error: Name 'fail' is not defined
+c.py:1: error: Name 'fail' is not defined
+
+[case testTomlFilesCmdlineOverridesConfigPyprojectTOML]
+# cmd: mypy override.py
+[file pyproject.toml]
+\[tool.mypy]
+files = "config.py"
+[out]
+mypy: can't read file 'override.py': No such file or directory
+== Return code: 2
+
+[case testDuplicateModulesPyprojectTOML]
+# cmd: mypy src
+[file pyproject.toml]
+\[tool.mypy]
+mypy_path = "src"
+[file src/__init__.py]
+[file src/a.py]
+import foo.bar
+[file src/foo/__init__.py]
+[file src/foo/bar.py]
+1+'x'
+[out]
+src/foo/bar.py: error: Source file found twice under different module names: 'src.foo.bar' and 'foo.bar'
+== Return code: 2
+
+[case testCmdlinePackageAndTomlFilesPyprojectTOML]
+# cmd: mypy -p pkg
+[file pyproject.toml]
+\[tool.mypy]
+files = "file"
+[file pkg.py]
+x = 0  # type: str
+[file file.py]
+y = 0  # type: str
+[out]
+pkg.py:1: error: Incompatible types in assignment (expression has type "int", variable has type "str")
+
+[case testCmdlineModuleAndTomlFilesPyprojectTOML]
+# cmd: mypy -m pkg
+[file pyproject.toml]
+\[tool.mypy]
+files = "file"
+[file pkg.py]
+x = 0  # type: str
+[file file.py]
+y = 0  # type: str
+[out]
+pkg.py:1: error: Incompatible types in assignment (expression has type "int", variable has type "str")
diff --git a/test-data/unit/daemon.pyproject.test b/test-data/unit/daemon.pyproject.test
new file mode 100644
index 000000000000..2689482b41ee
--- /dev/null
+++ b/test-data/unit/daemon.pyproject.test
@@ -0,0 +1,63 @@
+-- End-to-end test cases for the daemon (dmypy).
+-- These are special because they run multiple shell commands.
+
+[case testDaemonIgnoreConfigFilesPyprojectTOMLPyprojectTOML]
+$ dmypy start -- --follow-imports=error
+Daemon started
+[file pyproject.toml]
+\[tool.mypy]
+files = './foo.py'
+
+[case testDaemonRunRestartPluginVersionPyprojectTOMLPyprojectTOML]
+$ dmypy run -- foo.py --no-error-summary
+Daemon started
+$ {python} -c "print(' ')" >> plug.py
+$ dmypy run -- foo.py --no-error-summary
+Restarting: plugins changed
+Daemon stopped
+Daemon started
+$ dmypy stop
+Daemon stopped
+[file pyproject.toml]
+\[tool.mypy]
+follow_imports = "error"
+plugins = "plug.py"
+[file foo.py]
+pass
+[file plug.py]
+from mypy.plugin import Plugin
+class Dummy(Plugin): pass
+def plugin(version): return Dummy
+
+[case testDaemonRunRestartGlobsPyprojectTOMLPyprojectTOML]
+-- Ensure dmypy is not restarted if the configuration doesn't change and it contains globs
+-- Note: Backslash path separator in output is replaced with forward slash so the same test succeeds on Windows as well
+$ dmypy run -- foo --follow-imports=error --python-version=3.6
+Daemon started
+foo/lol.py:1: error: Name 'fail' is not defined
+Found 1 error in 1 file (checked 3 source files)
+== Return code: 1
+$ dmypy run -- foo --follow-imports=error --python-version=3.6
+foo/lol.py:1: error: Name 'fail' is not defined
+Found 1 error in 1 file (checked 3 source files)
+== Return code: 1
+$ {python} -c "print('[mypy]')" >mypy.ini
+$ {python} -c "print('ignore_errors=True')" >>mypy.ini
+$ dmypy run -- foo --follow-imports=error --python-version=3.6
+Restarting: configuration changed
+Daemon stopped
+Daemon started
+Success: no issues found in 3 source files
+$ dmypy stop
+Daemon stopped
+[file pyproject.toml]
+\[tool.mypy]
+ignore_errors = true
+\[tool.'mypy-*.lol']
+ignore_errors = false
+
+[file foo/__init__.py]
+[file foo/lol.py]
+fail
+[file foo/ok.py]
+a: int = 1
diff --git a/test-data/unit/diff.pyproject.test b/test-data/unit/diff.pyproject.test
new file mode 100644
index 000000000000..8da407d06fd2
--- /dev/null
+++ b/test-data/unit/diff.pyproject.test
@@ -0,0 +1,41 @@
+-- Test cases for taking a diff of two module ASTs/symbol tables.
+-- The diffs are used for fined-grained incremental checking.
+
+[case testDynamicBasePluginDiffPyprojectTOML]
+# flags: --config-file tmp/pyproject.toml
+from mod import declarative_base, Column, Instr
+
+Base = declarative_base()
+
+class Model(Base):
+    x: Column[int]
+class Other:
+    x: Column[int]
+class Diff:
+    x: Column[int]
+[file next.py]
+from mod import declarative_base, Column, Instr
+
+Base = declarative_base()
+
+class Model(Base):
+    x: Column[int]
+class Other:
+    x: Column[int]
+class Diff(Base):
+    x: Column[int]
+[file mod.py]
+from typing import Generic, TypeVar
+def declarative_base(): ...
+
+T = TypeVar('T')
+
+class Column(Generic[T]): ...
+class Instr(Generic[T]): ...
+
+[file pyproject.toml]
+\[tool.mypy]
+plugins = '<ROOT>/test-data/unit/plugins/dyn_class.py'
+[out]
+__main__.Diff
+__main__.Diff.x
diff --git a/test-data/unit/fine-grained.pyproject.test b/test-data/unit/fine-grained.pyproject.test
new file mode 100644
index 000000000000..a20070dbbf8a
--- /dev/null
+++ b/test-data/unit/fine-grained.pyproject.test
@@ -0,0 +1,245 @@
+-- Test cases for fine-grained incremental checking
+--
+-- Test cases may define multiple versions of a file
+-- (e.g. m.py, m.py.2). There is always an initial batch
+-- pass that processes all files present initially, followed
+-- by one or more fine-grained incremental passes that use
+-- alternative versions of files, if available. If a file
+-- just has a single .py version, it is used for all passes.
+
+-- TODO: what if version for some passes but not all
+
+-- Output is laid out like this:
+--
+--   [out]
+--   <optional output from batch pass>
+--   ==
+--   <optional output from first incremental pass>
+--
+--
+-- Modules that are expected to be detected as changed by dmypy_server
+-- can be checked with [stale ...]
+-- Generally this should mean added, deleted, or changed files, though there
+-- are important edge cases related to the cache: deleted files won't be detected
+-- as changed in the initial run with the cache while modules that depended on them
+-- should be.
+--
+-- Modules that are require a full-module reprocessing by update can be checked with
+-- [rechecked ...]. This should include any files detected as having changed as well
+-- as any files that contain targets that need to be reprocessed but which haven't
+-- been loaded yet. If there is no [rechecked...] directive, it inherits the value of
+-- [stale ...].
+--
+-- Specifications for later runs can be given with [stale2 ...], [stale3 ...], etc.
+--
+-- Test runner can parse options from pyproject.toml file. Updating this file in
+-- between incremental runs is not yet supported.
+--
+-- Each test case run without caching and with caching (if the initial run passes),
+-- unless it has one a -only_when_cache or -only_when_nocache arguments. We sometimes
+-- skip caching test cases to speed up tests, if the caching variant is not useful.
+-- The caching test case variants get an implicit _cached suffix.
+
+[case testPerFileStrictOptionalModulePyprojectTOML]
+import a
+[file pyproject.toml]
+\[tool.mypy]
+strict_optional = false
+\[tool.'mypy-a.*']
+strict_optional = true
+[file a.py]
+from typing import Optional
+import b
+x: int
+y: int = x
+[file b.py]
+from typing import Optional
+x: int
+y: int = x
+[file b.py.2]
+from typing import Optional
+x: Optional[int]
+y: int = x
+[file a.py.3]
+from typing import Optional
+import b
+x: Optional[int]
+y: int = x
+[out]
+==
+==
+a.py:4: error: Incompatible types in assignment (expression has type "Optional[int]", variable has type "int")
+
+[case testPerFileStrictOptionalModuleOnlyPyprojectTOML]
+import a
+[file pyproject.toml]
+\[tool.mypy]
+strict_optional = false
+\[tool.'mypy-a.*']
+strict_optional = true
+[file a.py]
+from typing import Optional
+import b
+y: int = b.x
+class Dummy:
+    def f(self) -> None:
+        pass
+[file b.py]
+from typing import Optional
+import c
+x: int
+y: int = c.x
+class Dummy:
+    def f(self) -> None:
+        pass
+[file c.py]
+from typing import Optional
+x: int
+[file c.py.2]
+from typing import Optional
+x: Optional[int]
+[file b.py.3]
+from typing import Optional
+import c
+x: Optional[int]
+y: int = c.x
+[file a.py.4]
+from typing import Optional
+import b
+y: Optional[int] = b.x
+class Dummy:
+    def f(self) -> None:
+        pass
+[out]
+==
+==
+a.py:3: error: Incompatible types in assignment (expression has type "Optional[int]", variable has type "int")
+==
+
+[case testPerFileStrictOptionalFunctionPyprojectTOML]
+import a
+[file pyproject.toml]
+\[tool.mypy]
+strict_optional = false
+\[tool.'mypy-b.*']
+strict_optional = true
+[file a.py]
+from typing import Optional
+import b
+def f() -> None:
+    x: int
+    x = b.g(x)
+[file b.py]
+from typing import Optional
+import c
+def g(x: Optional[int]) -> Optional[int]:
+    return c.h(x)
+[file c.py]
+from typing import Optional
+def h(x: Optional[int]) -> int:
+    pass
+[file c.py.2]
+from typing import Optional
+def h(x: int) -> int:
+    pass
+[file b.py.3]
+from typing import Optional
+import c
+def g(x: int) -> Optional[int]:
+    return c.h(x)
+[out]
+==
+b.py:4: error: Argument 1 to "h" has incompatible type "Optional[int]"; expected "int"
+==
+
+[case testPerFileStrictOptionalMethodPyprojectTOML]
+import a
+[file pyproject.toml]
+\[tool.mypy]
+strict_optional = false
+\[tool.'mypy-b.*']
+strict_optional = true
+[file a.py]
+from typing import Optional
+import b
+class A:
+    def f(self) -> None:
+        x: int
+        x = b.B().g(x)
+[file b.py]
+from typing import Optional
+import c
+class B:
+    def g(self, x: Optional[int]) -> Optional[int]:
+        return c.C().h(x)
+[file c.py]
+from typing import Optional
+class C:
+    def h(self, x: Optional[int]) -> int:
+        pass
+[file c.py.2]
+from typing import Optional
+class C:
+    def h(self, x: int) -> int:
+        pass
+[file b.py.3]
+from typing import Optional
+import c
+class B:
+    def g(self, x: int) -> Optional[int]:
+        return c.C().h(x)
+[out]
+==
+b.py:5: error: Argument 1 to "h" of "C" has incompatible type "Optional[int]"; expected "int"
+==
+
+[case testRefreshIgnoreErrors1PyprojectTOML]
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.mypy-b]
+ignore_errors = true
+[file a.py]
+y = '1'
+[file a.py.2]
+y = 1
+[file b.py]
+from a import y
+def fu() -> None:
+    1+'lurr'
+    y
+[out]
+==
+
+[case testRefreshIgnoreErrors2PyprojectTOML]
+[file pyproject.toml]
+\[tool.mypy]
+\[tool.mypy-b]
+ignore_errors = true
+[file b.py]
+def fu() -> int:
+    1+'lurr'
+    return 1
+[file b.py.2]
+def fu() -> int:
+    1+'lurr'
+    return 2
+[out]
+==
+
+[case testRefreshOptionsPyprojectTOML]
+[file pyproject.toml]
+\[tool.mypy]
+disallow_any_generics = true
+\[tool.mypy-b]
+disallow_any_generics = false
+[file a.py]
+y = '1'
+[file a.py.2]
+y = 1
+[file b.py]
+from typing import List
+from a import y
+x = []  # type: List
+[builtins fixtures/list.pyi]
+[out]
+==
diff --git a/test-data/unit/pep561.pyproject.test b/test-data/unit/pep561.pyproject.test
new file mode 100644
index 000000000000..31b7206c05f4
--- /dev/null
+++ b/test-data/unit/pep561.pyproject.test
@@ -0,0 +1,27 @@
+[case testTypedPkg_config_nositepackagesPyprojectTOML]
+# pkgs: typedpkg
+from typedpkg.sample import ex
+from typedpkg import dne
+a = ex([''])
+reveal_type(a)
+[file pyproject.toml]
+\[tool.mypy]
+no_site_packages = true
+[out]
+testTypedPkg_config_nositepackagesPyprojectTOML.py:2: error: Cannot find implementation or library stub for module named "typedpkg.sample"
+testTypedPkg_config_nositepackagesPyprojectTOML.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
+testTypedPkg_config_nositepackagesPyprojectTOML.py:3: error: Cannot find implementation or library stub for module named "typedpkg"
+testTypedPkg_config_nositepackagesPyprojectTOML.py:5: note: Revealed type is 'Any'
+
+[case testTypedPkgNoSitePkgsIgnoredImportsPyprojectTOML]
+# pkgs: typedpkg
+# flags: --no-site-packages
+from typedpkg.sample import ex
+from typedpkg import dne
+a = ex([''])
+reveal_type(a)
+[file pyproject.toml]
+\[tool.mypy]
+ignore_missing_imports = true
+[out]
+testTypedPkgNoSitePkgsIgnoredImportsPyprojectTOML.py:6: note: Revealed type is 'Any'
diff --git a/test-data/unit/reports.pyproject.test b/test-data/unit/reports.pyproject.test
new file mode 100644
index 000000000000..95e41797d296
--- /dev/null
+++ b/test-data/unit/reports.pyproject.test
@@ -0,0 +1,12 @@
+-- Tests for reports
+--
+-- This file follows syntax of cmdline.test.
+
+[case testConfigErrorUnknownReportPyprojectTOML]
+# cmd: mypy -c pass
+[file pyproject.toml]
+\[tool.mypy]
+bad_report = "."
+[out]
+pyproject.toml: [mypy]: Unrecognized report type: bad_report
+== Return code: 0


Code review comments:
diff_hunk: @@ -100,7 +122,22 @@ def check_follow_imports(choice: str) -> str:
     'cache_dir': expand_path,
     'python_executable': expand_path,
     'strict': bool,
-}  # type: Final
+}  # type: Final[Dict[str, Any]]
+
+# Reuse the ini_config_types and overwrite the diff
+toml_config_types = copy.deepcopy(ini_config_types)  # type: Final[Dict[str, Any]]
path: mypy/config_parser.py
text: @user1:
This seems to be the issue. Copying a compiled lambda doesn't work because of a mypyc bug: https://github.com/mypyc/mypyc/issues/804#issuecomment-802947572

Can you try replacing this with `copy.copy`? It should work.