├── .flake8 ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── flake8_mypy.py ├── mypy_default.ini ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── clash ├── collections.py └── london_calling.py ├── invalid_types.py ├── profile_run.py ├── relative_imports.py └── test_mypy.py /.flake8: -------------------------------------------------------------------------------- 1 | # This is an example .flake8 config, used when developing bugbear itself. 2 | # Keep in sync with setup.cfg which is used for source packages. 3 | 4 | [flake8] 5 | ignore = E302, E501 6 | max-line-length = 80 7 | max-complexity = 12 8 | select = B,C,E,F,T4,W,B9 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.5 4 | - 3.6 5 | - 3.6-dev 6 | - 3.7-dev 7 | - 3.7 8 | install: 9 | - python3 -m pip install -U git+git://github.com/python/mypy.git 10 | - pip install -e . 11 | script: python setup.py test 12 | 13 | # use unsupported xenial 14 | dist: xenial 15 | sudo: required 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Łukasz Langa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst *.md 2 | recursive-include tests *.txt *.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flake8-mypy 2 | 3 | ## NOTE: THIS PROJECT IS DEAD 4 | 5 | It was created in early 2017 when Mypy performance was often insufficient for 6 | in-editor linting. The Flake8 plugin traded correctness for performance, 7 | making Mypy only check a bare minimum of problems. These days Mypy is 8 | accelerated with mypyc, as well as uses aggressive caching to speed up 9 | incremental checks. It's no longer worth it to use hacks such as flake8-mypy. 10 | 11 | ## What was the project anyway? 12 | 13 | A plugin for [Flake8](http://flake8.pycqa.org/) integrating 14 | [mypy](http://mypy-lang.org/). The idea is to enable limited type 15 | checking as a linter inside editors and other tools that already support 16 | *Flake8* warning syntax and config. 17 | 18 | 19 | ## List of warnings 20 | 21 | *flake8-mypy* reserves **T4** for all current and future codes, T being 22 | the natural letter for typing-related errors. There are other plugins 23 | greedily reserving the entire letter **T**. To this I say: `¯\_(ツ)_/¯`. 24 | 25 | **T400**: any typing note. 26 | 27 | **T484**: any typing error (after PEP 484, geddit?). 28 | 29 | **T498**: internal *mypy* error. 30 | 31 | **T499**: internal *mypy* traceback, stderr output, or an unmatched line. 32 | 33 | I plan to support more fine-grained error codes for specific *mypy* 34 | errors in the future. 35 | 36 | 37 | ## Two levels of type checking 38 | 39 | *mypy* shines when given a full program to analyze. You can then use 40 | options like `--follow-imports` or `--disallow-untyped-calls` to 41 | exercise the full transitive closure of your modules, catching errors 42 | stemming from bad API usage or incompatible types. That being said, 43 | those checks take time, and require access to the entire codebase. For 44 | some tools, like an editor with an open file, or a code review tool, 45 | achieving this is not trivial. This is where a more limited approach 46 | inside a linter comes in. 47 | 48 | *Flake8* operates on unrelated files, it doesn't perform full program 49 | analysis. In other words, it doesn't follow imports. This is a curse 50 | and a blessing. We cannot find complex problems and the number of 51 | warnings we can safely show without risking false positives is lower. 52 | In return, we can provide useful warnings with great performance, usable 53 | for realtime editor integration. 54 | 55 | As it turns out, in this mode of operation, *mypy* is still able to 56 | provide useful information on the annotations within and at least usage 57 | of stubbed standard library and third party libraries. However, for 58 | best effects, you will want to use separate configuration for *mypy*'s 59 | standalone mode and for usage as a *Flake8* plugin. 60 | 61 | 62 | ## Configuration 63 | 64 | Due to the reasoning above, by default *flake8-mypy* will operate with 65 | options equivalent to the following: 66 | 67 | ```ini 68 | [mypy] 69 | # Specify the target platform details in config, so your developers are 70 | # free to run mypy on Windows, Linux, or macOS and get consistent 71 | # results. 72 | python_version=3.6 73 | platform=linux 74 | 75 | # flake8-mypy expects the two following for sensible formatting 76 | show_column_numbers=True 77 | show_error_context=False 78 | 79 | # do not follow imports (except for ones found in typeshed) 80 | follow_imports=skip 81 | 82 | # since we're ignoring imports, writing .mypy_cache doesn't make any sense 83 | cache_dir=/dev/null 84 | 85 | # suppress errors about unsatisfied imports 86 | ignore_missing_imports=True 87 | 88 | # allow untyped calls as a consequence of the options above 89 | disallow_untyped_calls=False 90 | 91 | # allow returning Any as a consequence of the options above 92 | warn_return_any=False 93 | 94 | # treat Optional per PEP 484 95 | strict_optional=True 96 | 97 | # ensure all execution paths are returning 98 | warn_no_return=True 99 | 100 | # lint-style cleanliness for typing needs to be disabled; returns more errors 101 | # than the full run. 102 | warn_redundant_casts=False 103 | warn_unused_ignores=False 104 | 105 | # The following are off by default since they're too noisy. 106 | # Flip them on if you feel adventurous. 107 | disallow_untyped_defs=False 108 | check_untyped_defs=False 109 | ``` 110 | 111 | If you disagree with the defaults above, you can specify your own *mypy* 112 | configuration by providing the `--mypy-config=` command-line option to 113 | *Flake8* (with the .flake8/setup.cfg equivalent being called 114 | `mypy_config`). The value of that option should be a path to a mypy.ini 115 | or setup.cfg compatible file. For full configuration syntax, follow 116 | [mypy documentation](http://mypy.readthedocs.io/en/latest/config_file.html). 117 | 118 | For the sake of simplicity and readability, the config you provide will 119 | fully replace the one listed above. Values left out will be using 120 | *mypy*'s own defaults. 121 | 122 | Remember that for the best user experience, your linter integration mode 123 | shouldn't generally display errors that a full run of *mypy* wouldn't. 124 | This would be confusing. 125 | 126 | Note: chaing the `follow_imports` option might have surprising effects. 127 | If the file you're linting with Flake8 has other files around it, then in 128 | "silent" or "normal" mode those files will be used to follow imports. 129 | This includes imports from [typeshed](https://github.com/python/typeshed/). 130 | 131 | 132 | ## Tests 133 | 134 | Just run: 135 | 136 | ``` 137 | python setup.py test 138 | ``` 139 | 140 | ## OMG, this is Python 3 only! 141 | 142 | Yes, so is *mypy*. Relax, you can run *Flake8* with all popular plugins 143 | **as a tool** perfectly fine under Python 3.5+ even if you want to 144 | analyze Python 2 code. This way you'll be able to parse all of the new 145 | syntax supported on Python 3 but also *effectively all* the Python 2 146 | syntax at the same time. 147 | 148 | By making the code exclusively Python 3.5+, I'm able to focus on the 149 | quality of the checks and re-use all the nice features of the new 150 | releases (check out [pathlib](https://docs.python.org/3/library/pathlib.html)) 151 | instead of wasting cycles on Unicode compatibility, etc. 152 | 153 | 154 | ## License 155 | 156 | MIT 157 | 158 | 159 | ## Change Log 160 | 161 | ### 17.8.0 162 | 163 | * avoid raising errors in the default config which don't happen during 164 | a full run (disable warn_unused_ignores and warn_redundant_casts) 165 | 166 | * always run type checks from a temporary directory to avoid 167 | clashing with unrelated files in the same directory 168 | 169 | ### 17.3.3 170 | 171 | * suppress *mypy* messages about relative imports 172 | 173 | ### 17.3.2 174 | 175 | * bugfix: using *Flake8* with absolute paths now correctly matches *mypy* 176 | messages 177 | 178 | * bugfix: don't crash on relative imports in the form `from . import X` 179 | 180 | ### 17.3.1 181 | 182 | * switch `follow_imports` from "silent" to "skip" to avoid name clashing 183 | files being used to follow imports within 184 | [typeshed](https://github.com/python/typeshed/) 185 | 186 | * set MYPYPATH by default to give stubs from typeshed higher priority 187 | than local sources 188 | 189 | ### 17.3.0 190 | 191 | * performance optimization: skip running *mypy* over files that contain 192 | no annotations or imports from `typing` 193 | 194 | * bugfix: when running over an entire directory, T484 is now correctly 195 | used instead of T499 196 | 197 | ### 17.2.0 198 | 199 | * first published version 200 | 201 | * date-versioned 202 | 203 | 204 | ## Authors 205 | 206 | Glued together by [Łukasz Langa](mailto:lukasz@langa.pl). 207 | -------------------------------------------------------------------------------- /flake8_mypy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import ast 3 | from collections import namedtuple 4 | from functools import partial 5 | import itertools 6 | import logging 7 | import os 8 | from pathlib import Path 9 | import re 10 | from tempfile import NamedTemporaryFile, TemporaryDirectory 11 | import time 12 | import traceback 13 | from typing import ( 14 | Any, 15 | Iterator, 16 | List, 17 | Optional, 18 | Pattern, 19 | Tuple, 20 | Type, 21 | TYPE_CHECKING, 22 | Union, 23 | ) 24 | 25 | import attr 26 | import mypy.api 27 | 28 | if TYPE_CHECKING: 29 | import flake8.options.manager.OptionManager # noqa 30 | 31 | 32 | __version__ = '17.8.0' 33 | 34 | 35 | noqa = re.compile(r'# noqa\b', re.I).search 36 | Error = namedtuple('Error', 'lineno col message type vars') 37 | 38 | 39 | def make_arguments(**kwargs: Union[str, bool]) -> List[str]: 40 | result = [] 41 | for k, v in kwargs.items(): 42 | k = k.replace('_', '-') 43 | if v is True: 44 | result.append('--' + k) 45 | elif v is False: 46 | continue 47 | else: 48 | result.append('--{}={}'.format(k, v)) 49 | return result 50 | 51 | 52 | def calculate_mypypath() -> List[str]: 53 | """Return MYPYPATH so that stubs have precedence over local sources.""" 54 | 55 | typeshed_root = None 56 | count = 0 57 | started = time.time() 58 | for parent in itertools.chain( 59 | # Look in current script's parents, useful for zipapps. 60 | Path(__file__).parents, 61 | # Look around site-packages, useful for virtualenvs. 62 | Path(mypy.api.__file__).parents, 63 | # Look in global paths, useful for globally installed. 64 | Path(os.__file__).parents, 65 | ): 66 | count += 1 67 | candidate = parent / 'lib' / 'mypy' / 'typeshed' 68 | if candidate.is_dir(): 69 | typeshed_root = candidate 70 | break 71 | 72 | # Also check the non-installed path, useful for `setup.py develop`. 73 | candidate = parent / 'typeshed' 74 | if candidate.is_dir(): 75 | typeshed_root = candidate 76 | break 77 | 78 | LOG.debug( 79 | 'Checked %d paths in %.2fs looking for typeshed. Found %s', 80 | count, 81 | time.time() - started, 82 | typeshed_root, 83 | ) 84 | 85 | if not typeshed_root: 86 | return [] 87 | 88 | stdlib_dirs = ('3.7', '3.6', '3.5', '3.4', '3.3', '3.2', '3', '2and3') 89 | stdlib_stubs = [ 90 | typeshed_root / 'stdlib' / stdlib_dir 91 | for stdlib_dir in stdlib_dirs 92 | ] 93 | third_party_dirs = ('3.7', '3.6', '3', '2and3') 94 | third_party_stubs = [ 95 | typeshed_root / 'third_party' / tp_dir 96 | for tp_dir in third_party_dirs 97 | ] 98 | return [ 99 | str(p) for p in stdlib_stubs + third_party_stubs 100 | ] 101 | 102 | 103 | # invalid_types.py:5: error: Missing return statement 104 | MYPY_ERROR_TEMPLATE = r""" 105 | ^ 106 | .* # whatever at the beginning 107 | {filename}: # this needs to be provided in run() 108 | (?P\d+) # necessary for the match 109 | (:(?P\d+))? # optional but useful column info 110 | :[ ] # ends the preamble 111 | ((?Perror|warning|note):)? # optional class 112 | [ ](?P.*) # the rest 113 | $""" 114 | LOG = logging.getLogger('flake8.mypy') 115 | DEFAULT_ARGUMENTS = make_arguments( 116 | platform='linux', 117 | 118 | # flake8-mypy expects the two following for sensible formatting 119 | show_column_numbers=True, 120 | show_error_context=False, 121 | 122 | # suppress error messages from unrelated files 123 | follow_imports='skip', 124 | 125 | # since we're ignoring imports, writing .mypy_cache doesn't make any sense 126 | cache_dir=os.devnull, 127 | 128 | # suppress errors about unsatisfied imports 129 | ignore_missing_imports=True, 130 | 131 | # allow untyped calls as a consequence of the options above 132 | disallow_untyped_calls=False, 133 | 134 | # allow returning Any as a consequence of the options above 135 | warn_return_any=False, 136 | 137 | # treat Optional per PEP 484 138 | strict_optional=True, 139 | 140 | # ensure all execution paths are returning 141 | warn_no_return=True, 142 | 143 | # lint-style cleanliness for typing needs to be disabled; returns more errors 144 | # than the full run. 145 | warn_redundant_casts=False, 146 | warn_unused_ignores=False, 147 | 148 | # The following are off by default. Flip them on if you feel 149 | # adventurous. 150 | disallow_untyped_defs=False, 151 | check_untyped_defs=False, 152 | ) 153 | 154 | 155 | _Flake8Error = Tuple[int, int, str, Type['MypyChecker']] 156 | 157 | 158 | @attr.s(hash=False) 159 | class MypyChecker: 160 | name = 'flake8-mypy' 161 | version = __version__ 162 | 163 | tree = attr.ib(default=None) 164 | filename = attr.ib(default='(none)') 165 | lines = attr.ib(default=[]) # type: List[int] 166 | options = attr.ib(default=None) 167 | visitor = attr.ib(default=attr.Factory(lambda: TypingVisitor)) 168 | 169 | def run(self) -> Iterator[_Flake8Error]: 170 | if not self.lines: 171 | return # empty file, no need checking. 172 | 173 | visitor = self.visitor() 174 | visitor.visit(self.tree) 175 | 176 | if not visitor.should_type_check: 177 | return # typing not used in the module 178 | 179 | if not self.options.mypy_config and 'MYPYPATH' not in os.environ: 180 | os.environ['MYPYPATH'] = ':'.join(calculate_mypypath()) 181 | 182 | # Always put the file in a separate temporary directory to avoid 183 | # unexpected clashes with other .py and .pyi files in the same original 184 | # directory. 185 | with TemporaryDirectory(prefix='flake8mypy_') as d: 186 | file = NamedTemporaryFile( 187 | 'w', 188 | encoding='utf8', 189 | prefix='tmpmypy_', 190 | suffix='.py', 191 | dir=d, 192 | delete=False, 193 | ) 194 | try: 195 | self.filename = file.name 196 | for line in self.lines: 197 | file.write(line) 198 | file.close() 199 | yield from self._run() 200 | finally: 201 | os.remove(file.name) 202 | 203 | def _run(self) -> Iterator[_Flake8Error]: 204 | mypy_cmdline = self.build_mypy_cmdline(self.filename, self.options.mypy_config) 205 | mypy_re = self.build_mypy_re(self.filename) 206 | last_t499 = 0 207 | try: 208 | stdout, stderr, returncode = mypy.api.run(mypy_cmdline) 209 | except Exception as exc: 210 | # Pokémon exception handling to guard against mypy's internal errors 211 | last_t499 += 1 212 | yield self.adapt_error(T498(last_t499, 0, vars=(type(exc), str(exc)))) 213 | for line in traceback.format_exc().splitlines(): 214 | last_t499 += 1 215 | yield self.adapt_error(T499(last_t499, 0, vars=(line,))) 216 | else: 217 | # FIXME: should we make any decision based on `returncode`? 218 | for line in stdout.splitlines(): 219 | try: 220 | e = self.make_error(line, mypy_re) 221 | except ValueError: 222 | # unmatched line 223 | last_t499 += 1 224 | yield self.adapt_error(T499(last_t499, 0, vars=(line,))) 225 | continue 226 | 227 | if self.omit_error(e): 228 | continue 229 | 230 | yield self.adapt_error(e) 231 | 232 | for line in stderr.splitlines(): 233 | last_t499 += 1 234 | yield self.adapt_error(T499(last_t499, 0, vars=(line,))) 235 | 236 | @classmethod 237 | def adapt_error(cls, e: Any) -> _Flake8Error: 238 | """Adapts the extended error namedtuple to be compatible with Flake8.""" 239 | return e._replace(message=e.message.format(*e.vars))[:4] 240 | 241 | def omit_error(self, e: Error) -> bool: 242 | """Returns True if error should be ignored.""" 243 | if ( 244 | e.vars and 245 | e.vars[0] == 'No parent module -- cannot perform relative import' 246 | ): 247 | return True 248 | 249 | return bool(noqa(self.lines[e.lineno - 1])) 250 | 251 | @classmethod 252 | def add_options(cls, parser: 'flake8.options.manager.OptionManager') -> None: 253 | parser.add_option( 254 | '--mypy-config', 255 | parse_from_config=True, 256 | help="path to a custom mypy configuration file", 257 | ) 258 | 259 | def make_error(self, line: str, regex: Pattern) -> Error: 260 | m = regex.match(line) 261 | if not m: 262 | raise ValueError("unmatched line") 263 | 264 | lineno = int(m.group('lineno')) 265 | column = int(m.group('column') or 0) 266 | message = m.group('message').strip() 267 | if m.group('class') == 'note': 268 | return T400(lineno, column, vars=(message,)) 269 | 270 | return T484(lineno, column, vars=(message,)) 271 | 272 | def build_mypy_cmdline( 273 | self, filename: str, mypy_config: Optional[str] 274 | ) -> List[str]: 275 | if mypy_config: 276 | return ['--config-file=' + mypy_config, filename] 277 | 278 | return DEFAULT_ARGUMENTS + [filename] 279 | 280 | def build_mypy_re(self, filename: Union[str, Path]) -> Pattern: 281 | filename = Path(filename) 282 | if filename.is_absolute(): 283 | prefix = Path('.').absolute() 284 | try: 285 | filename = filename.relative_to(prefix) 286 | except ValueError: 287 | pass # not relative to the cwd 288 | 289 | re_filename = re.escape(str(filename)) 290 | if re_filename.startswith(r'\./'): 291 | re_filename = re_filename[3:] 292 | return re.compile( 293 | MYPY_ERROR_TEMPLATE.format(filename=re_filename), 294 | re.VERBOSE, 295 | ) 296 | 297 | 298 | @attr.s 299 | class TypingVisitor(ast.NodeVisitor): 300 | """Used to determine if the file is using annotations at all.""" 301 | should_type_check = attr.ib(default=False) 302 | 303 | def visit_FunctionDef(self, node: ast.FunctionDef) -> None: 304 | if node.returns: 305 | self.should_type_check = True 306 | return 307 | 308 | for arg in itertools.chain(node.args.args, node.args.kwonlyargs): 309 | if arg.annotation: 310 | self.should_type_check = True 311 | return 312 | 313 | va = node.args.vararg 314 | kw = node.args.kwarg 315 | if (va and va.annotation) or (kw and kw.annotation): 316 | self.should_type_check = True 317 | 318 | def visit_Import(self, node: ast.Import) -> None: 319 | for name in node.names: 320 | if ( 321 | isinstance(name, ast.alias) and 322 | name.name == 'typing' or 323 | name.name.startswith('typing.') 324 | ): 325 | self.should_type_check = True 326 | break 327 | 328 | def visit_ImportFrom(self, node: ast.ImportFrom) -> None: 329 | if ( 330 | node.level == 0 and 331 | node.module == 'typing' or 332 | node.module and node.module.startswith('typing.') 333 | ): 334 | self.should_type_check = True 335 | 336 | def generic_visit(self, node: ast.AST) -> None: 337 | """Called if no explicit visitor function exists for a node.""" 338 | for _field, value in ast.iter_fields(node): 339 | if self.should_type_check: 340 | break 341 | 342 | if isinstance(value, list): 343 | for item in value: 344 | if self.should_type_check: 345 | break 346 | if isinstance(item, ast.AST): 347 | self.visit(item) 348 | elif isinstance(value, ast.AST): 349 | self.visit(value) 350 | 351 | 352 | # Generic mypy error 353 | T484 = partial( 354 | Error, 355 | message="T484 {}", 356 | type=MypyChecker, 357 | vars=(), 358 | ) 359 | 360 | # Generic mypy note 361 | T400 = partial( 362 | Error, 363 | message="T400 note: {}", 364 | type=MypyChecker, 365 | vars=(), 366 | ) 367 | 368 | # Internal mypy error (summary) 369 | T498 = partial( 370 | Error, 371 | message="T498 Internal mypy error '{}': {}", 372 | type=MypyChecker, 373 | vars=(), 374 | ) 375 | 376 | # Internal mypy error (traceback, stderr, unmatched line) 377 | T499 = partial( 378 | Error, 379 | message="T499 {}", 380 | type=MypyChecker, 381 | vars=(), 382 | ) 383 | -------------------------------------------------------------------------------- /mypy_default.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Specify the target platform details in config, so your developers are 3 | # free to run mypy on Windows, Linux, or macOS and get consistent 4 | # results. 5 | platform=linux 6 | 7 | # flake8-mypy expects the two following for sensible formatting 8 | show_column_numbers=True 9 | show_error_context=False 10 | 11 | # suppress error messages from unrelated files 12 | follow_imports=skip 13 | 14 | # since we're ignoring imports, writing .mypy_cache doesn't make any sense 15 | cache_dir=/dev/null 16 | 17 | # suppress errors about unsatisfied imports 18 | ignore_missing_imports=True 19 | 20 | # allow untyped calls as a consequence of the options above 21 | disallow_untyped_calls=False 22 | 23 | # allow returning Any as a consequence of the options above 24 | warn_return_any=False 25 | 26 | # treat Optional per PEP 484 27 | strict_optional=True 28 | 29 | # ensure all execution paths are returning 30 | warn_no_return=True 31 | 32 | # lint-style cleanliness for typing needs to be disabled; returns more errors 33 | # than the full run. 34 | warn_redundant_casts=False 35 | warn_unused_ignores=False 36 | 37 | # The following are off by default since they're too noisy. 38 | # Flip them on if you feel adventurous. 39 | disallow_untyped_defs=False 40 | check_untyped_defs=False 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | python-tag = py35.py36 3 | 4 | [flake8] 5 | # Keep in sync with .flake8. This copy here is needed for source packages 6 | # to be able to pass tests without failing selfclean check. 7 | ignore = E302, E501 8 | max-line-length = 80 9 | max-complexity = 12 10 | select = B,C,E,F,T4,W,B9 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Łukasz Langa 2 | 3 | import ast 4 | import os 5 | import re 6 | from setuptools import setup 7 | import sys 8 | 9 | 10 | assert sys.version_info >= (3, 5, 0), "flake8-mypy requires Python 3.5+" 11 | 12 | 13 | current_dir = os.path.abspath(os.path.dirname(__file__)) 14 | readme_md = os.path.join(current_dir, 'README.md') 15 | try: 16 | import pypandoc 17 | long_description = pypandoc.convert_file(readme_md, 'rst') 18 | except(IOError, ImportError): 19 | print() 20 | print( 21 | '\x1b[31m\x1b[1mwarning:\x1b[0m\x1b[31m pandoc not found, ' 22 | 'long description will be ugly (PyPI does not support .md).' 23 | '\x1b[0m' 24 | ) 25 | print() 26 | with open(readme_md, encoding='utf8') as ld_file: 27 | long_description = ld_file.read() 28 | 29 | 30 | _version_re = re.compile(r'__version__\s+=\s+(?P.*)') 31 | 32 | 33 | with open(os.path.join(current_dir, 'flake8_mypy.py'), 'r', encoding='utf8') as f: 34 | version = _version_re.search(f.read()).group('version') 35 | version = str(ast.literal_eval(version)) 36 | 37 | 38 | setup( 39 | name='flake8-mypy', 40 | version=version, 41 | description="A plugin for flake8 integrating mypy.", 42 | long_description=long_description, 43 | keywords='flake8 mypy bugs linter qa typing', 44 | author='Łukasz Langa', 45 | author_email='lukasz@langa.pl', 46 | url='https://github.com/ambv/flake8-mypy', 47 | license='MIT', 48 | py_modules=['flake8_mypy'], 49 | zip_safe=False, 50 | install_requires=['flake8 >= 3.0.0', 'attrs', 'mypy'], 51 | test_suite='tests.test_mypy', 52 | classifiers=[ 53 | 'Development Status :: 3 - Alpha', 54 | 'Environment :: Console', 55 | 'Framework :: Flake8', 56 | 'Intended Audience :: Developers', 57 | 'License :: OSI Approved :: MIT License', 58 | 'Operating System :: OS Independent', 59 | 'Programming Language :: Python', 60 | 'Programming Language :: Python :: 3.5', 61 | 'Programming Language :: Python :: 3.6', 62 | 'Topic :: Software Development :: Libraries :: Python Modules', 63 | 'Topic :: Software Development :: Quality Assurance', 64 | ], 65 | entry_points={ 66 | 'flake8.extension': [ 67 | 'T4 = flake8_mypy:MypyChecker', 68 | ], 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambv/flake8-mypy/2b00b0133656af4da468a3d24935df71ffc34722/tests/__init__.py -------------------------------------------------------------------------------- /tests/clash/collections.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambv/flake8-mypy/2b00b0133656af4da468a3d24935df71ffc34722/tests/clash/collections.py -------------------------------------------------------------------------------- /tests/clash/london_calling.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from six.moves import UserDict 3 | 4 | 5 | def fun() -> Counter: 6 | return UserDict() 7 | -------------------------------------------------------------------------------- /tests/invalid_types.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | 5 | def no_return_value() -> int: 6 | print("Forgot to actually return!") 7 | 8 | 9 | def call_stdlib_and_wrong_type() -> str: 10 | return os.getpid() + " is my PID" 11 | 12 | 13 | def main() -> None: # type: ignore 14 | for num in range(100): 15 | print() 16 | 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /tests/profile_run.py: -------------------------------------------------------------------------------- 1 | import cProfile 2 | import pstats 3 | 4 | 5 | PROF_FILE = '/tmp/mypy-profile.prof' 6 | 7 | 8 | pr = cProfile.Profile() 9 | try: 10 | pr.enable() 11 | from flake8.main import cli 12 | cli.main() 13 | finally: 14 | pr.disable() 15 | pr.dump_stats(PROF_FILE) 16 | ps = pstats.Stats(PROF_FILE) 17 | ps.sort_stats('cumtime') 18 | ps.print_stats() 19 | -------------------------------------------------------------------------------- /tests/relative_imports.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from . import another_sibling_module 4 | from .. import another_parent_module 5 | 6 | 7 | def fun( 8 | elements: List[another_sibling_module.Element] 9 | ) -> another_parent_module.Class: 10 | return another_parent_module.Class(elements=elements) 11 | -------------------------------------------------------------------------------- /tests/test_mypy.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from pathlib import Path 3 | import subprocess 4 | from typing import List, Union 5 | import unittest 6 | from unittest import mock 7 | 8 | from flake8_mypy import TypingVisitor, MypyChecker, T484 9 | from flake8_mypy import Error, _Flake8Error 10 | 11 | 12 | class MypyTestCase(unittest.TestCase): 13 | maxDiff = None # type: int 14 | 15 | def errors(self, *errors: Error) -> List[_Flake8Error]: 16 | return [MypyChecker.adapt_error(e) for e in errors] 17 | 18 | def assert_visit(self, code: str, should_type_check: bool) -> None: 19 | tree = ast.parse(code) 20 | v = TypingVisitor() 21 | v.visit(tree) 22 | self.assertEqual(v.should_type_check, should_type_check) 23 | 24 | def get_mypychecker(self, file: Union[Path, str]) -> MypyChecker: 25 | current = Path('.').absolute() 26 | filename = Path(__file__).relative_to(current).parent / file 27 | with filename.open('r', encoding='utf8', errors='surrogateescape') as f: 28 | lines = f.readlines() 29 | options = mock.MagicMock() 30 | options.mypy_config = None 31 | return MypyChecker( 32 | filename=str(filename), 33 | lines=lines, 34 | tree=ast.parse(''.join(lines)), 35 | options=options, 36 | ) 37 | 38 | def test_imports(self) -> None: 39 | self.assert_visit("import os", False) 40 | self.assert_visit("import os.typing", False) 41 | self.assert_visit("from .typing import something", False) 42 | self.assert_visit("from something import typing", False) 43 | self.assert_visit("from . import typing", False) 44 | 45 | self.assert_visit("import typing", True) 46 | self.assert_visit("import typing.io", True) 47 | self.assert_visit("import one, two, three, typing", True) 48 | self.assert_visit("from typing import List", True) 49 | self.assert_visit("from typing.io import IO", True) 50 | 51 | def test_functions(self) -> None: 52 | self.assert_visit("def f(): ...", False) 53 | self.assert_visit("def f(a): ...", False) 54 | self.assert_visit("def f(a, b=None): ...", False) 55 | self.assert_visit("def f(a, *, b=None): ...", False) 56 | self.assert_visit("def f(a, *args, **kwargs): ...", False) 57 | 58 | self.assert_visit("def f() -> None: ...", True) 59 | self.assert_visit("def f(a: str): ...", True) 60 | self.assert_visit("def f(a, b: str = None): ...", True) 61 | self.assert_visit("def f(a, *, b: str = None): ...", True) 62 | self.assert_visit("def f(a, *args: str, **kwargs: str): ...", True) 63 | 64 | def test_invalid_types(self) -> None: 65 | mpc = self.get_mypychecker('invalid_types.py') 66 | errors = list(mpc.run()) 67 | self.assertEqual( 68 | errors, 69 | self.errors( 70 | T484(5, 1, vars=('Missing return statement',)), 71 | T484( 72 | 10, 73 | 5, 74 | vars=( 75 | 'Incompatible return value type (got "int", expected "str")', 76 | ), 77 | ), 78 | T484( 79 | 10, 80 | 12, 81 | vars=( 82 | 'Unsupported operand types for + ("int" and "str")', 83 | ), 84 | ), 85 | ), 86 | ) 87 | 88 | def test_clash(self) -> None: 89 | """We set MYPYPATH to prioritize typeshed over local modules.""" 90 | mpc = self.get_mypychecker('clash/london_calling.py') 91 | errors = list(mpc.run()) 92 | self.assertEqual( 93 | errors, 94 | self.errors( 95 | T484( 96 | 6, 97 | 5, 98 | vars=( 99 | 'Incompatible return value type ' 100 | '(got "UserDict[, ]", ' 101 | 'expected "Counter[Any]")', 102 | ), 103 | ), 104 | ), 105 | ) 106 | 107 | def test_relative_imports(self) -> None: 108 | mpc = self.get_mypychecker('relative_imports.py') 109 | errors = list(mpc.run()) 110 | self.assertEqual(errors, []) 111 | 112 | def test_selfclean_flake8_mypy(self) -> None: 113 | filename = Path(__file__).absolute().parent.parent / 'flake8_mypy.py' 114 | proc = subprocess.run( 115 | ['flake8', '-j0', str(filename)], 116 | stdout=subprocess.PIPE, 117 | stderr=subprocess.PIPE, 118 | timeout=60, 119 | ) 120 | msgs = "\n\n".join((proc.stdout.decode('utf8'), proc.stderr.decode('utf8'))) 121 | self.assertEqual(proc.returncode, 0, msgs) 122 | self.assertEqual(proc.stdout, b'') 123 | # self.assertEqual(proc.stderr, b'') 124 | 125 | def test_selfclean_test_mypy(self) -> None: 126 | filename = Path(__file__).absolute() 127 | proc = subprocess.run( 128 | ['flake8', '-j0', str(filename)], 129 | stdout=subprocess.PIPE, 130 | stderr=subprocess.PIPE, 131 | timeout=60, 132 | ) 133 | msgs = "\n\n".join((proc.stdout.decode('utf8'), proc.stderr.decode('utf8'))) 134 | self.assertEqual(proc.returncode, 0, msgs) 135 | self.assertEqual(proc.stdout, b'') 136 | # self.assertEqual(proc.stderr, b'') 137 | 138 | 139 | if __name__ == '__main__': 140 | unittest.main() 141 | --------------------------------------------------------------------------------