diff options
| author | Anthony Sottile <asottile@umich.edu> | 2019-05-19 17:01:14 -0700 |
|---|---|---|
| committer | Anthony Sottile <asottile@umich.edu> | 2019-05-19 17:31:04 -0700 |
| commit | fb7e9338cd06760a2f9096f976f0e246fc36a09e (patch) | |
| tree | c2a2a2a907d7540eef0dbd633d6cb52cc50da14a | |
| parent | b6ba6d4d03109965d3cf5174d5c2e6868d7d92bb (diff) | |
| download | flake8-fb7e9338cd06760a2f9096f976f0e246fc36a09e.tar.gz | |
mypy now passes
32 files changed, 255 insertions, 212 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4b1c28d..cbccb83 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,7 @@ python36: script: tox -e py36 python37: - image: python:3.7-rc + image: python:3.7 stage: test script: tox -e py37 @@ -44,6 +44,11 @@ linters: stage: test script: tox -e linters +pre-commit: + image: python:3.7 + stage: test + script: tox -e pre-commit + docs: stage: test script: tox -e docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..48f8a1d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.701 + hooks: + - id: mypy + exclude: ^(docs/|example-plugin/|tests/fixtures) diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 3d1cd43..0000000 --- a/mypy.ini +++ /dev/null @@ -1,2 +0,0 @@ -[mypy] -ignore_missing_imports = true @@ -16,3 +16,28 @@ requires-dist = pyflakes >= 2.1.0, < 2.2.0 pycodestyle >= 2.5.0, < 2.6.0 mccabe >= 0.6.0, < 0.7.0 + +[mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +# TODO: disallow_untyped_defs = true +no_implicit_optional = true +warn_unused_ignores = true + +# TODO: until we opt in all the modules +[mypy-flake8.defaults] +disallow_untyped_defs = true +[mypy-flake8.exceptions] +disallow_untyped_defs = true +[mypy-flake8.formatting.*] +disallow_untyped_defs = true +[mypy-flake8.main.cli] +disallow_untyped_defs = true +[mypy-flake8.statistics] +disallow_untyped_defs = true +[mypy-flake8.utils] +disallow_untyped_defs = true + +[mypy-tests.*] +disallow_untyped_defs = false diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 73ec63a..8394276 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -12,6 +12,9 @@ This module import logging import sys +if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 + from typing import Type # `typing.Type` was introduced in 3.5.2 + LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) @@ -61,7 +64,7 @@ def configure_logging(verbosity, filename=None, logformat=LOG_FORMAT): if not filename or filename in ("stderr", "stdout"): fileobj = getattr(sys, filename or "stderr") - handler_cls = logging.StreamHandler + handler_cls = logging.StreamHandler # type: Type[logging.Handler] else: fileobj = filename handler_cls = logging.FileHandler diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 4fb106e..fd5658a 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -3,14 +3,13 @@ import collections import errno import logging import signal -import sys import tokenize -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple try: import multiprocessing except ImportError: - multiprocessing = None + multiprocessing = None # type: ignore from flake8 import defaults from flake8 import exceptions @@ -73,8 +72,7 @@ class Manager(object): self.options = style_guide.options self.checks = checker_plugins self.jobs = self._job_count() - self.processes = [] - self.checkers = [] + self.checkers = [] # type: List[FileChecker] self.statistics = { "files": 0, "logical lines": 0, @@ -195,7 +193,7 @@ class Manager(object): ) def make_checkers(self, paths=None): - # type: (List[str]) -> None + # type: (Optional[List[str]]) -> None """Create checkers for each file.""" if paths is None: paths = self.arguments @@ -270,8 +268,10 @@ class Manager(object): def run_parallel(self): """Run the checkers in parallel.""" - final_results = collections.defaultdict(list) - final_statistics = collections.defaultdict(dict) + # fmt: off + final_results = collections.defaultdict(list) # type: Dict[str, List[Tuple[str, int, int, str, Optional[str]]]] # noqa: E501 + final_statistics = collections.defaultdict(dict) # type: Dict[str, Dict[str, None]] # noqa: E501 + # fmt: on try: pool = multiprocessing.Pool(self.jobs, _pool_init) @@ -281,6 +281,7 @@ class Manager(object): self.run_serial() return + pool_closed = False try: pool_map = pool.imap_unordered( _run_checks, @@ -295,9 +296,9 @@ class Manager(object): final_statistics[filename] = statistics pool.close() pool.join() - pool = None + pool_closed = True finally: - if pool is not None: + if not pool_closed: pool.terminate() pool.join() @@ -345,9 +346,6 @@ class Manager(object): def stop(self): """Stop checking files.""" self._process_statistics() - for proc in self.processes: - LOG.info("Joining %s to the main process", proc.name) - proc.join() class FileChecker(object): @@ -370,7 +368,9 @@ class FileChecker(object): self.options = options self.filename = filename self.checks = checks - self.results = [] + # fmt: off + self.results = [] # type: List[Tuple[str, int, int, str, Optional[str]]] # noqa: E501 + # fmt: on self.statistics = { "tokens": 0, "logical lines": 0, @@ -389,17 +389,17 @@ class FileChecker(object): return "FileChecker for {}".format(self.filename) def _make_processor(self): + # type: () -> Optional[processor.FileProcessor] try: return processor.FileProcessor(self.filename, self.options) - except IOError: + except IOError as e: # If we can not read the file due to an IOError (e.g., the file # does not exist or we do not have the permissions to open it) # then we need to format that exception for the user. # NOTE(sigmavirus24): Historically, pep8 has always reported this # as an E902. We probably *want* a better error code for this # going forward. - (exc_type, exception) = sys.exc_info()[:2] - message = "{0}: {1}".format(exc_type.__name__, exception) + message = "{0}: {1}".format(type(e).__name__, e) self.report("E902", 0, 0, message) return None @@ -446,7 +446,7 @@ class FileChecker(object): token = () if len(exception.args) > 1: token = exception.args[1] - if len(token) > 2: + if token and len(token) > 2: row, column = token[1:3] else: row, column = (1, 0) @@ -482,14 +482,10 @@ class FileChecker(object): """Run all checks expecting an abstract syntax tree.""" try: ast = self.processor.build_ast() - except (ValueError, SyntaxError, TypeError): - (exc_type, exception) = sys.exc_info()[:2] - row, column = self._extract_syntax_information(exception) + except (ValueError, SyntaxError, TypeError) as e: + row, column = self._extract_syntax_information(e) self.report( - "E999", - row, - column, - "%s: %s" % (exc_type.__name__, exception.args[0]), + "E999", row, column, "%s: %s" % (type(e).__name__, e.args[0]) ) return diff --git a/src/flake8/exceptions.py b/src/flake8/exceptions.py index bc44d8f..53ca4b7 100644 --- a/src/flake8/exceptions.py +++ b/src/flake8/exceptions.py @@ -1,5 +1,10 @@ """Exception classes for all of Flake8.""" +from typing import Dict + +if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 + from flake8.plugins.manager import Plugin + class Flake8Exception(Exception): """Plain Flake8 exception.""" @@ -19,13 +24,14 @@ class FailedToLoadPlugin(Flake8Exception): FORMAT = 'Flake8 failed to load plugin "%(name)s" due to %(exc)s.' def __init__(self, plugin, exception): + # type: (Plugin, Exception) -> None """Initialize our FailedToLoadPlugin exception.""" self.plugin = plugin self.ep_name = self.plugin.name self.original_exception = exception super(FailedToLoadPlugin, self).__init__(plugin, exception) - def __str__(self): + def __str__(self): # type: () -> str """Format our exception message.""" return self.FORMAT % { "name": self.ep_name, @@ -47,7 +53,7 @@ class InvalidSyntax(Flake8Exception): self.column_number = 0 super(InvalidSyntax, self).__init__(exception) - def __str__(self): + def __str__(self): # type: () -> str """Format our exception message.""" return self.error_message @@ -58,6 +64,7 @@ class PluginRequestedUnknownParameters(Flake8Exception): FORMAT = '"%(name)s" requested unknown parameters causing %(exc)s' def __init__(self, plugin, exception): + # type: (Dict[str, str], Exception) -> None """Pop certain keyword arguments for initialization.""" self.plugin = plugin self.original_exception = exception @@ -65,7 +72,7 @@ class PluginRequestedUnknownParameters(Flake8Exception): plugin, exception ) - def __str__(self): + def __str__(self): # type: () -> str """Format our exception message.""" return self.FORMAT % { "name": self.plugin["plugin_name"], @@ -79,12 +86,13 @@ class PluginExecutionFailed(Flake8Exception): FORMAT = '"%(name)s" failed during execution due to "%(exc)s"' def __init__(self, plugin, exception): + # type: (Dict[str, str], Exception) -> None """Utilize keyword arguments for message generation.""" self.plugin = plugin self.original_exception = exception super(PluginExecutionFailed, self).__init__(plugin, exception) - def __str__(self): + def __str__(self): # type: () -> str """Format our exception message.""" return self.FORMAT % { "name": self.plugin["plugin_name"], @@ -99,40 +107,33 @@ class HookInstallationError(Flake8Exception): class GitHookAlreadyExists(HookInstallationError): """Exception raised when the git pre-commit hook file already exists.""" - def __init__(self, *args, **kwargs): - """Initialize the path attribute.""" - self.path = kwargs.pop("path") - super(GitHookAlreadyExists, self).__init__(*args, **kwargs) - - def __str__(self): - """Provide a nice message regarding the exception.""" - msg = ( + def __init__(self, path): # type: (str) -> None + """Initialize the exception message from the `path`.""" + self.path = path + tmpl = ( "The Git pre-commit hook ({0}) already exists. To convince " "Flake8 to install the hook, please remove the existing " "hook." ) - return msg.format(self.path) + super(GitHookAlreadyExists, self).__init__(tmpl.format(self.path)) class MercurialHookAlreadyExists(HookInstallationError): """Exception raised when a mercurial hook is already configured.""" - hook_name = None + hook_name = None # type: str - def __init__(self, *args, **kwargs): + def __init__(self, path, value): # type: (str, str) -> None """Initialize the relevant attributes.""" - self.path = kwargs.pop("path") - self.value = kwargs.pop("value") - super(MercurialHookAlreadyExists, self).__init__(*args, **kwargs) - - def __str__(self): - """Return a nicely formatted string for these errors.""" - msg = ( + self.path = path + self.value = value + tmpl = ( 'The Mercurial {0} hook already exists with "{1}" in {2}. ' "To convince Flake8 to install the hook, please remove the " "{0} configuration from the [hooks] section of your hgrc." ) - return msg.format(self.hook_name, self.value, self.path) + msg = tmpl.format(self.hook_name, self.value, self.path) + super(MercurialHookAlreadyExists, self).__init__(msg) class MercurialCommitHookAlreadyExists(MercurialHookAlreadyExists): diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index 259d03f..388ab0e 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -1,6 +1,13 @@ """The base class and interface for all formatting plugins.""" from __future__ import print_function +import optparse +from typing import IO, List, Optional, Tuple + +if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 + from flake8.statistics import Statistics + from flake8.style_guide import Violation + class BaseFormatter(object): """Class defining the formatter interface. @@ -25,6 +32,7 @@ class BaseFormatter(object): """ def __init__(self, options): + # type: (optparse.Values) -> None """Initialize with the options parsed from config and cli. This also calls a hook, :meth:`after_init`, so subclasses do not need @@ -36,33 +44,30 @@ class BaseFormatter(object): """ self.options = options self.filename = options.output_file - self.output_fd = None + self.output_fd = None # type: Optional[IO[str]] self.newline = "\n" self.after_init() - def after_init(self): + def after_init(self): # type: () -> None """Initialize the formatter further.""" - pass - def beginning(self, filename): + def beginning(self, filename): # type: (str) -> None """Notify the formatter that we're starting to process a file. :param str filename: The name of the file that Flake8 is beginning to report results from. """ - pass - def finished(self, filename): + def finished(self, filename): # type: (str) -> None """Notify the formatter that we've finished processing a file. :param str filename: The name of the file that Flake8 has finished reporting results from. """ - pass - def start(self): + def start(self): # type: () -> None """Prepare the formatter to receive input. This defaults to initializing :attr:`output_fd` if :attr:`filename` @@ -70,7 +75,7 @@ class BaseFormatter(object): if self.filename: self.output_fd = open(self.filename, "a") - def handle(self, error): + def handle(self, error): # type: (Violation) -> None """Handle an error reported by Flake8. This defaults to calling :meth:`format`, :meth:`show_source`, and @@ -87,7 +92,7 @@ class BaseFormatter(object): source = self.show_source(error) self.write(line, source) - def format(self, error): + def format(self, error): # type: (Violation) -> Optional[str] """Format an error reported by Flake8. This method **must** be implemented by subclasses. @@ -106,7 +111,7 @@ class BaseFormatter(object): "Subclass of BaseFormatter did not implement" " format." ) - def show_statistics(self, statistics): + def show_statistics(self, statistics): # type: (Statistics) -> None """Format and print the statistics.""" for error_code in statistics.error_codes(): stats_for_error_code = statistics.statistics_for(error_code) @@ -122,6 +127,7 @@ class BaseFormatter(object): ) def show_benchmarks(self, benchmarks): + # type: (List[Tuple[str, float]]) -> None """Format and print the benchmarks.""" # NOTE(sigmavirus24): The format strings are a little confusing, even # to me, so here's a quick explanation: @@ -142,7 +148,7 @@ class BaseFormatter(object): benchmark = float_format(statistic=statistic, value=value) self._write(benchmark) - def show_source(self, error): + def show_source(self, error): # type: (Violation) -> Optional[str] """Show the physical line generating the error. This also adds an indicator for the particular part of the line that @@ -170,7 +176,7 @@ class BaseFormatter(object): # one return error.physical_line + pointer - def _write(self, output): + def _write(self, output): # type: (str) -> None """Handle logic of whether to use an output file or print().""" if self.output_fd is not None: self.output_fd.write(output + self.newline) @@ -178,6 +184,7 @@ class BaseFormatter(object): print(output, end=self.newline) def write(self, line, source): + # type: (Optional[str], Optional[str]) -> None """Write the line either to the output file or stdout. This handles deciding whether to write to a file or print to standard @@ -195,7 +202,7 @@ class BaseFormatter(object): if source: self._write(source) - def stop(self): + def stop(self): # type: () -> None """Clean up after reporting is finished.""" if self.output_fd is not None: self.output_fd.close() diff --git a/src/flake8/formatting/default.py b/src/flake8/formatting/default.py index e1061f3..55a5d01 100644 --- a/src/flake8/formatting/default.py +++ b/src/flake8/formatting/default.py @@ -1,6 +1,11 @@ """Default formatting class for Flake8.""" +from typing import Optional, Set + from flake8.formatting import base +if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 + from flake8.style_guide import Violation + class SimpleFormatter(base.BaseFormatter): """Simple abstraction for Default and Pylint formatter commonality. @@ -18,9 +23,9 @@ class SimpleFormatter(base.BaseFormatter): """ - error_format = None + error_format = None # type: str - def format(self, error): + def format(self, error): # type: (Violation) -> Optional[str] """Format and write error out. If an output filename is specified, write formatted errors to that @@ -44,7 +49,7 @@ class Default(SimpleFormatter): error_format = "%(path)s:%(row)d:%(col)d: %(code)s %(text)s" - def after_init(self): + def after_init(self): # type: () -> None """Check for a custom format string.""" if self.options.format.lower() != "default": self.error_format = self.options.format @@ -61,28 +66,27 @@ class FilenameOnly(SimpleFormatter): error_format = "%(path)s" - def after_init(self): + def after_init(self): # type: () -> None """Initialize our set of filenames.""" - self.filenames_already_printed = set() + self.filenames_already_printed = set() # type: Set[str] - def show_source(self, error): + def show_source(self, error): # type: (Violation) -> Optional[str] """Do not include the source code.""" - pass - def format(self, error): + def format(self, error): # type: (Violation) -> Optional[str] """Ensure we only print each error once.""" if error.filename not in self.filenames_already_printed: self.filenames_already_printed.add(error.filename) return super(FilenameOnly, self).format(error) + else: + return None class Nothing(base.BaseFormatter): """Print absolutely nothing.""" - def format(self, error): + def format(self, error): # type: (Violation) -> Optional[str] """Do nothing.""" - pass - def show_source(self, error): + def show_source(self, error): # type: (Violation) -> Optional[str] """Do not print the source.""" - pass diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index c7859b9..f3c5382 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -2,9 +2,10 @@ from __future__ import print_function import logging +import optparse import sys import time -from typing import List, Optional +from typing import Dict, List, Optional, Set import flake8 from flake8 import checker @@ -18,11 +19,8 @@ from flake8.options import manager from flake8.plugins import manager as plugin_manager if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 - # fmt: off - # `typing.Type` as introduced in 3.5.2 - from typing import Type # noqa: F401 (until flake8 3.7) - from flake8.formatting.base import BaseFormatter # noqa: F401, E501 (until flake8 3.7) - # fmt: on + from typing import Type # `typing.Type` was introduced in 3.5.2 + from flake8.formatting.base import BaseFormatter LOG = logging.getLogger(__name__) @@ -32,7 +30,6 @@ class Application(object): """Abstract our application into a class.""" def __init__(self, program="flake8", version=flake8.__version__): - # type: (str, str) -> None """Initialize our application. :param str program: @@ -43,7 +40,7 @@ class Application(object): #: The timestamp when the Application instance was instantiated. self.start_time = time.time() #: The timestamp when the Application finished reported errors. - self.end_time = None + self.end_time = None # type: float #: The name of the program being run self.program = program #: The version of the program being run @@ -56,33 +53,35 @@ class Application(object): options.register_default_options(self.option_manager) #: The preliminary options parsed from CLI before plugins are loaded, #: into a :class:`optparse.Values` instance - self.prelim_opts = None + self.prelim_opts = None # type: optparse.Values #: The preliminary arguments parsed from CLI before plugins are loaded - self.prelim_args = None + self.prelim_args = None # type: List[str] #: The instance of :class:`flake8.options.config.ConfigFileFinder` self.config_finder = None #: The :class:`flake8.options.config.LocalPlugins` found in config - self.local_plugins = None + self.local_plugins = None # type: config.LocalPlugins #: The instance of :class:`flake8.plugins.manager.Checkers` - self.check_plugins = None + self.check_plugins = None # type: plugin_manager.Checkers + # fmt: off #: The instance of :class:`flake8.plugins.manager.ReportFormatters` - self.formatting_plugins = None + self.formatting_plugins = None # type: plugin_manager.ReportFormatters + # fmt: on #: The user-selected formatter from :attr:`formatting_plugins` - self.formatter = None + self.formatter = None # type: BaseFormatter #: The :class:`flake8.style_guide.StyleGuideManager` built from the #: user's options - self.guide = None + self.guide = None # type: style_guide.StyleGuideManager #: The :class:`flake8.checker.Manager` that will handle running all of #: the checks selected by the user. - self.file_checker_manager = None + self.file_checker_manager = None # type: checker.Manager #: The user-supplied options parsed into an instance of #: :class:`optparse.Values` - self.options = None + self.options = None # type: optparse.Values #: The left over arguments that were not parsed by #: :attr:`option_manager` - self.args = None + self.args = None # type: List[str] #: The number of errors, warnings, and other messages after running #: flake8 and taking into account ignored errors and lines. self.result_count = 0 @@ -96,7 +95,7 @@ class Application(object): #: Whether the program is processing a diff or not self.running_against_diff = False #: The parsed diff information - self.parsed_diff = {} + self.parsed_diff = {} # type: Dict[str, Set[int]] def parse_preliminary_options_and_args(self, argv=None): # type: (Optional[List[str]]) -> None diff --git a/src/flake8/main/git.py b/src/flake8/main/git.py index eed5c09..683396a 100644 --- a/src/flake8/main/git.py +++ b/src/flake8/main/git.py @@ -90,9 +90,7 @@ def install(): os.path.join(hooks_directory, "pre-commit") ) if os.path.exists(pre_commit_file): - raise exceptions.GitHookAlreadyExists( - "File already exists", path=pre_commit_file - ) + raise exceptions.GitHookAlreadyExists(path=pre_commit_file) executable = get_executable() diff --git a/src/flake8/main/mercurial.py b/src/flake8/main/mercurial.py index 65ef8ce..4ea410a 100644 --- a/src/flake8/main/mercurial.py +++ b/src/flake8/main/mercurial.py @@ -7,6 +7,7 @@ import configparser import os import subprocess +from typing import Set from flake8 import exceptions as exc @@ -101,7 +102,7 @@ def install(): def get_filenames_from(repository, kwargs): - seen_filenames = set() + seen_filenames = set() # type: Set[str] node = kwargs["node"] for revision in range(repository[node], len(repository)): for filename in repository[revision].files(): diff --git a/src/flake8/main/setuptools_command.py b/src/flake8/main/setuptools_command.py index 1b57c0d..55dc378 100644 --- a/src/flake8/main/setuptools_command.py +++ b/src/flake8/main/setuptools_command.py @@ -1,6 +1,6 @@ """The logic for Flake8's integration with setuptools.""" import os -from typing import List +from typing import List, Tuple import setuptools @@ -48,7 +48,7 @@ class Flake8(setuptools.Command): def package_files(self): """Collect the files/dirs included in the registered modules.""" - seen_package_directories = () + seen_package_directories = () # type: Tuple[str, ...] directories = self.distribution.package_dir or {} empty_directory_exists = "" in directories packages = self.distribution.packages or [] diff --git a/src/flake8/main/vcs.py b/src/flake8/main/vcs.py index f7f139a..398643c 100644 --- a/src/flake8/main/vcs.py +++ b/src/flake8/main/vcs.py @@ -17,7 +17,7 @@ def install(option, option_string, value, parser): For more information about the callback signature, see: https://docs.python.org/3/library/optparse.html#optparse-option-callbacks """ - installer = _INSTALLERS.get(value) + installer = _INSTALLERS[value] errored = False successful = False try: diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index 468341b..026618c 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -4,6 +4,7 @@ import configparser import logging import os.path import sys +from typing import Dict, List, Sequence, Tuple, Union from flake8 import utils @@ -54,12 +55,15 @@ class ConfigFileFinder(object): # caches to avoid double-reading config files self._local_configs = None - self._local_found_files = [] + self._local_found_files = [] # type: List[str] self._user_config = None - self._cli_configs = {} + # fmt: off + self._cli_configs = {} # type: Dict[str, configparser.RawConfigParser] + # fmt: on @staticmethod def _read_config(files): + # type: (Union[Sequence[str], str]) -> Tuple[configparser.RawConfigParser, List[str]] # noqa: E501 config = configparser.RawConfigParser() if isinstance(files, (str, type(u""))): files = [files] @@ -83,6 +87,7 @@ class ConfigFileFinder(object): return (config, found_files) def cli_config(self, files): + # type: (str) -> configparser.RawConfigParser """Read and parse the config file specified on the command-line.""" if files not in self._cli_configs: config, found_files = self._read_config(files) @@ -379,7 +384,7 @@ def get_local_plugins(config_finder, cli_config=None, isolated=False): raw_paths = utils.parse_comma_separated_list( config.get(section, "paths").strip() ) - norm_paths = [] + norm_paths = [] # type: List[str] for base_dir in base_dirs: norm_paths.extend( path diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 0ded13a..5d21127 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -2,6 +2,7 @@ import collections import logging import optparse # pylint: disable=deprecated-module +from typing import Any, Callable, Dict, List, Optional, Set from flake8 import utils @@ -108,7 +109,7 @@ class Option(object): self.comma_separated_list = comma_separated_list self.normalize_paths = normalize_paths - self.config_name = None + self.config_name = None # type: Optional[str] if parse_from_config: if not long_option_name: raise ValueError( @@ -143,7 +144,7 @@ class Option(object): """Normalize the value based on the option configuration.""" if self.normalize_paths: # Decide whether to parse a list of paths or a single path - normalize = utils.normalize_path + normalize = utils.normalize_path # type: Callable[..., Any] if self.comma_separated_list: normalize = utils.normalize_paths return normalize(value, *normalize_args) @@ -200,13 +201,13 @@ class OptionManager(object): self.parser = optparse.OptionParser( prog=prog, version=version, usage=usage ) - self.config_options_dict = {} - self.options = [] + self.config_options_dict = {} # type: Dict[str, Option] + self.options = [] # type: List[Option] self.program_name = prog self.version = version - self.registered_plugins = set() - self.extended_default_ignore = set() - self.extended_default_select = set() + self.registered_plugins = set() # type: Set[PluginVersion] + self.extended_default_ignore = set() # type: Set[str] + self.extended_default_select = set() # type: Set[str] @staticmethod def format_plugin(plugin): @@ -231,6 +232,7 @@ class OptionManager(object): self.options.append(option) if option.parse_from_config: name = option.config_name + assert name is not None # nosec (for mypy) self.config_options_dict[name] = option self.config_options_dict[name.replace("_", "-")] = option LOG.debug('Registered option "%s".', option) @@ -326,7 +328,7 @@ class OptionManager(object): values = self.parser.get_default_values() self.parser.rargs = rargs - self.parser.largs = largs = [] + largs = [] # type: List[str] self.parser.values = values while rargs: @@ -340,7 +342,8 @@ class OptionManager(object): optparse.BadOptionError, optparse.OptionValueError, ) as err: - self.parser.largs.append(err.opt_str) + # TODO: https://gitlab.com/pycqa/flake8/issues/541 + largs.append(err.opt_str) # type: ignore args = largs + rargs options, xargs = self.parser.check_values(values, args) diff --git a/src/flake8/plugins/manager.py b/src/flake8/plugins/manager.py index 303c0f9..1554aa2 100644 --- a/src/flake8/plugins/manager.py +++ b/src/flake8/plugins/manager.py @@ -1,17 +1,12 @@ """Plugin loading and management logic and classes.""" import logging -import sys +from typing import Any, Dict, List, Set import entrypoints from flake8 import exceptions from flake8 import utils -if sys.version_info >= (3, 3): - import collections.abc as collections_abc -else: - import collections as collections_abc - LOG = logging.getLogger(__name__) __all__ = ("Checkers", "Plugin", "PluginManager", "ReportFormatters") @@ -37,7 +32,7 @@ class Plugin(object): self.name = name self.entry_point = entry_point self.local = local - self._plugin = None + self._plugin = None # type: Any self._parameters = None self._parameter_names = None self._group = None @@ -236,8 +231,8 @@ class PluginManager(object): # pylint: disable=too-few-public-methods Plugins from config (as "X = path.to:Plugin" strings). """ self.namespace = namespace - self.plugins = {} - self.names = [] + self.plugins = {} # type: Dict[str, Plugin] + self.names = [] # type: List[str] self._load_local_plugins(local_plugins or []) self._load_entrypoint_plugins() @@ -310,7 +305,7 @@ class PluginManager(object): # pylint: disable=too-few-public-methods :rtype: tuple """ - plugins_seen = set() + plugins_seen = set() # type: Set[str] for entry_point_name in self.names: plugin = self.plugins[entry_point_name] plugin_name = plugin.plugin_name @@ -345,7 +340,7 @@ def version_for(plugin): class PluginTypeManager(object): """Parent class for most of the specific plugin types.""" - namespace = None + namespace = None # type: str def __init__(self, local_plugins=None): """Initialize the plugin type's manager. @@ -398,9 +393,7 @@ class PluginTypeManager(object): def _generate_call_function(method_name, optmanager, *args, **kwargs): def generated_function(plugin): # noqa: D105 method = getattr(plugin, method_name, None) - if method is not None and isinstance( - method, collections_abc.Callable - ): + if method is not None and callable(method): return method(optmanager, *args, **kwargs) return generated_function diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 2f233c4..9be0c81 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -10,6 +10,7 @@ except ImportError: else: demandimport.disable() import os +from typing import List import pyflakes import pyflakes.checker @@ -59,8 +60,8 @@ class FlakesChecker(pyflakes.checker.Checker): name = "pyflakes" version = pyflakes.__version__ with_doctest = False - include_in_doctest = [] - exclude_from_doctest = [] + include_in_doctest = [] # type: List[str] + exclude_from_doctest = [] # type: List[str] def __init__(self, tree, file_tokens, filename): """Initialize the PyFlakes plugin with an AST tree and filename.""" diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 09185e5..239f908 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -3,7 +3,7 @@ import contextlib import logging import sys import tokenize -from typing import List +from typing import Any, Dict, List, Tuple import flake8 from flake8 import defaults @@ -66,9 +66,9 @@ class FileProcessor(object): #: Number of blank lines self.blank_lines = 0 #: Checker states for each plugin? - self._checker_states = {} + self._checker_states = {} # type: Dict[str, Dict[Any, Any]] #: Current checker state - self.checker_state = None + self.checker_state = None # type: Dict[Any, Any] #: User provided option for hang closing self.hang_closing = options.hang_closing #: Character used for indentation @@ -93,8 +93,10 @@ class FileProcessor(object): self.previous_logical = "" #: Previous unindented (i.e. top-level) logical line self.previous_unindented_logical_line = "" + # fmt: off #: Current set of tokens - self.tokens = [] + self.tokens = [] # type: List[Tuple[int, str, Tuple[int, int], Tuple[int, int], str]] # noqa: E501 + # fmt: on #: Total number of lines in the file self.total_lines = len(self.lines) #: Verbosity level of Flake8 diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index f2131b5..100227f 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -1,15 +1,19 @@ """Statistic collection logic for Flake8.""" import collections +from typing import Dict, Generator, List, Optional + +if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 + from flake8.style_guide import Violation class Statistics(object): """Manager of aggregated statistics for a run of Flake8.""" - def __init__(self): + def __init__(self): # type: () -> None """Initialize the underlying dictionary for our statistics.""" - self._store = {} + self._store = {} # type: Dict[Key, Statistic] - def error_codes(self): + def error_codes(self): # type: () -> List[str] """Return all unique error codes stored. :returns: @@ -19,7 +23,7 @@ class Statistics(object): """ return sorted({key.code for key in self._store}) - def record(self, error): + def record(self, error): # type: (Violation) -> None """Add the fact that the error was seen in the file. :param error: @@ -34,6 +38,7 @@ class Statistics(object): self._store[key].increment() def statistics_for(self, prefix, filename=None): + # type: (str, Optional[str]) -> Generator[Statistic, None, None] """Generate statistics for the prefix and filename. If you have a :class:`Statistics` object that has recorded errors, @@ -74,11 +79,11 @@ class Key(collections.namedtuple("Key", ["filename", "code"])): __slots__ = () @classmethod - def create_from(cls, error): + def create_from(cls, error): # type: (Violation) -> Key """Create a Key from :class:`flake8.style_guide.Violation`.""" return cls(filename=error.filename, code=error.code) - def matches(self, prefix, filename): + def matches(self, prefix, filename): # type: (str, Optional[str]) -> bool """Determine if this key matches some constraints. :param str prefix: @@ -106,6 +111,7 @@ class Statistic(object): """ def __init__(self, error_code, filename, message, count): + # type: (str, str, str, int) -> None """Initialize our Statistic.""" self.error_code = error_code self.filename = filename @@ -113,7 +119,7 @@ class Statistic(object): self.count = count @classmethod - def create_from(cls, error): + def create_from(cls, error): # type: (Violation) -> Statistic """Create a Statistic from a :class:`flake8.style_guide.Violation`.""" return cls( error_code=error.code, @@ -122,6 +128,6 @@ class Statistic(object): count=0, ) - def increment(self): + def increment(self): # type: () -> None """Increment the number of times we've seen this error in this file.""" self.count += 1 diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index c617f5a..b644f8b 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -7,7 +7,7 @@ import itertools import linecache import logging import sys -from typing import Optional, Union +from typing import Dict, List, Optional, Set, Union from flake8 import defaults from flake8 import statistics @@ -68,7 +68,7 @@ class Violation(_Violation): """Class representing a violation reported by Flake8.""" def is_inline_ignored(self, disable_noqa): - # type: (Violation) -> bool + # type: (bool) -> bool """Determine if a comment has been added to ignore this line. :param bool disable_noqa: @@ -154,7 +154,7 @@ class DecisionEngine(object): def __init__(self, options): """Initialize the engine.""" - self.cache = {} + self.cache = {} # type: Dict[str, Decision] self.selected = tuple(options.select) self.extended_selected = tuple( sorted(options.extended_default_select, reverse=True) @@ -332,7 +332,7 @@ class StyleGuideManager(object): self.formatter = formatter self.stats = statistics.Statistics() self.decider = decider or DecisionEngine(options) - self.style_guides = [] + self.style_guides = [] # type: List[StyleGuide] self.default_style_guide = StyleGuide( options, formatter, self.stats, decider=decider ) @@ -390,7 +390,7 @@ class StyleGuideManager(object): text, physical_line=None, ): - # type: (str, str, int, int, str, Optional[str]) -> int + # type: (str, str, int, Optional[int], str, Optional[str]) -> int """Handle an error reported by a check. :param str code: @@ -419,6 +419,7 @@ class StyleGuideManager(object): ) def add_diff_ranges(self, diffinfo): + # type: (Dict[str, Set[int]]) -> None """Update the StyleGuides to filter out information not in the diff. This provides information to the underlying StyleGuides so that only @@ -448,7 +449,7 @@ class StyleGuide(object): self.filename = filename if self.filename: self.filename = utils.normalize_path(self.filename) - self._parsed_diff = {} + self._parsed_diff = {} # type: Dict[str, Set[int]] def __repr__(self): """Make it easier to debug which StyleGuide we're using.""" @@ -514,7 +515,7 @@ class StyleGuide(object): text, physical_line=None, ): - # type: (str, str, int, int, str, Optional[str]) -> int + # type: (str, str, int, Optional[int], str, Optional[str]) -> int """Handle an error reported by a check. :param str code: @@ -567,6 +568,7 @@ class StyleGuide(object): return 0 def add_diff_ranges(self, diffinfo): + # type: (Dict[str, Set[int]]) -> None """Update the StyleGuide to filter out information not in the diff. This provides information to the StyleGuide so that only the errors diff --git a/src/flake8/utils.py b/src/flake8/utils.py index b95d4db..f822388 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -3,13 +3,14 @@ import collections import fnmatch as _fnmatch import inspect import io +import logging import os import platform import re import sys import tokenize -from typing import Callable, Dict, Generator, List, Pattern, Sequence, Set -from typing import Tuple, Union +from typing import Callable, Dict, Generator, List, Optional, Pattern +from typing import Sequence, Set, Tuple, Union from flake8 import exceptions @@ -19,6 +20,7 @@ if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 DIFF_HUNK_REGEXP = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$") COMMA_SEPARATED_LIST_RE = re.compile(r"[,\s]") LOCAL_PLUGIN_LIST_RE = re.compile(r"[,\t\n\r\f\v]") +string_types = (str, type(u"")) def parse_comma_separated_list(value, regexp=COMMA_SEPARATED_LIST_RE): @@ -40,7 +42,7 @@ def parse_comma_separated_list(value, regexp=COMMA_SEPARATED_LIST_RE): if not value: return [] - if not isinstance(value, (list, tuple)): + if isinstance(value, string_types): value = regexp.split(value) item_gen = (item.strip() for item in value) @@ -77,8 +79,8 @@ def _tokenize_files_to_codes_mapping(value): return tokens -def parse_files_to_codes_mapping(value): # noqa: C901 - # type: (Union[Sequence[str], str]) -> List[Tuple[List[str], List[str]]] +def parse_files_to_codes_mapping(value_): # noqa: C901 + # type: (Union[Sequence[str], str]) -> List[Tuple[str, List[str]]] """Parse a files-to-codes maping. A files-to-codes mapping a sequence of values specified as @@ -88,20 +90,22 @@ def parse_files_to_codes_mapping(value): # noqa: C901 :param value: String to be parsed and normalized. :type value: str """ - if isinstance(value, (list, tuple)): - value = "\n".join(value) + if not isinstance(value_, string_types): + value = "\n".join(value_) + else: + value = value_ - ret = [] + ret = [] # type: List[Tuple[str, List[str]]] if not value.strip(): return ret class State: seen_sep = True seen_colon = False - filenames = [] - codes = [] + filenames = [] # type: List[str] + codes = [] # type: List[str] - def _reset(): + def _reset(): # type: () -> None if State.codes: for filename in State.filenames: ret.append((filename, State.codes)) @@ -110,11 +114,8 @@ def parse_files_to_codes_mapping(value): # noqa: C901 State.filenames = [] State.codes = [] - def _unexpected_token(): - # type: () -> exceptions.ExecutionError - - def _indent(s): - # type: (str) -> str + def _unexpected_token(): # type: () -> exceptions.ExecutionError + def _indent(s): # type: (str) -> str return " " + s.strip().replace("\n", "\n ") return exceptions.ExecutionError( @@ -192,7 +193,7 @@ def normalize_path(path, parent=os.curdir): return path.rstrip(separator + alternate_separator) -def _stdin_get_value_py3(): +def _stdin_get_value_py3(): # type: () -> io.StringIO stdin_value = sys.stdin.buffer.read() fd = io.BytesIO(stdin_value) try: @@ -211,13 +212,13 @@ def stdin_get_value(): stdin_value = io.BytesIO(sys.stdin.read()) else: stdin_value = _stdin_get_value_py3() - stdin_get_value.cached_stdin = stdin_value - cached_value = stdin_get_value.cached_stdin + stdin_get_value.cached_stdin = stdin_value # type: ignore + cached_value = stdin_get_value.cached_stdin # type: ignore return cached_value.getvalue() def parse_unified_diff(diff=None): - # type: (str) -> Dict[str, Set[int]] + # type: (Optional[str]) -> Dict[str, Set[int]] """Parse the unified diff passed on stdin. :returns: @@ -231,7 +232,7 @@ def parse_unified_diff(diff=None): number_of_rows = None current_path = None - parsed_paths = collections.defaultdict(set) + parsed_paths = collections.defaultdict(set) # type: Dict[str, Set[int]] for line in diff.splitlines(): if number_of_rows: # NOTE(sigmavirus24): Below we use a slice because stdin may be @@ -279,6 +280,7 @@ def parse_unified_diff(diff=None): 1 if not group else int(group) for group in hunk_match.groups() ] + assert current_path is not None # nosec (for mypy) parsed_paths[current_path].update( range(row, row + number_of_rows) ) @@ -338,12 +340,12 @@ def is_using_stdin(paths): return "-" in paths -def _default_predicate(*args): +def _default_predicate(*args): # type: (*str) -> bool return False def filenames_from(arg, predicate=None): - # type: (str, Callable[[str], bool]) -> Generator + # type: (str, Optional[Callable[[str], bool]]) -> Generator[str, None, None] # noqa: E501 """Generate filenames from an argument. :param str arg: @@ -384,8 +386,8 @@ def filenames_from(arg, predicate=None): yield arg -def fnmatch(filename, patterns, default=True): - # type: (str, List[str], bool) -> bool +def fnmatch(filename, patterns): + # type: (str, List[str]) -> bool """Wrap :func:`fnmatch.fnmatch` to add some functionality. :param str filename: @@ -399,7 +401,7 @@ def fnmatch(filename, patterns, default=True): ``default`` if patterns is empty. """ if not patterns: - return default + return True return any(_fnmatch.fnmatch(filename, pattern) for pattern in patterns) @@ -452,6 +454,7 @@ def parameters_for(plugin): def matches_filename(path, patterns, log_message, logger): + # type: (str, List[str], str, logging.Logger) -> bool """Use fnmatch to discern if a path exists in patterns. :param str path: @@ -483,7 +486,7 @@ def matches_filename(path, patterns, log_message, logger): return match -def get_python_version(): +def get_python_version(): # type: () -> str """Find and format the python implementation and version. :returns: diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..f7ac891 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""This is here because mypy doesn't understand PEP 420.""" diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 0a3703c..a17e5cd 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -216,9 +216,7 @@ def test_report_order(results, expected_order): # _handle_results is the first place which gets the sorted result # Should something non-private be mocked instead? - handler = mock.Mock() - handler.side_effect = count_side_effect - manager._handle_results = handler - - assert manager.report() == (len(results), len(results)) - handler.assert_called_once_with('placeholder', expected_results) + handler = mock.Mock(side_effect=count_side_effect) + with mock.patch.object(manager, '_handle_results', handler): + assert manager.report() == (len(results), len(results)) + handler.assert_called_once_with('placeholder', expected_results) diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py index f28ab80..ec051a8 100644 --- a/tests/unit/test_base_formatter.py +++ b/tests/unit/test_base_formatter.py @@ -44,7 +44,9 @@ def test_format_needs_to_be_implemented(): """Ensure BaseFormatter#format raises a NotImplementedError.""" formatter = base.BaseFormatter(options()) with pytest.raises(NotImplementedError): - formatter.format('foo') + formatter.format( + style_guide.Violation('A000', 'file.py', 1, 1, 'error text', None) + ) def test_show_source_returns_nothing_when_not_showing_source(): @@ -73,6 +75,7 @@ def test_show_source_updates_physical_line_appropriately(line, column): formatter = base.BaseFormatter(options(show_source=True)) error = style_guide.Violation('A000', 'file.py', 1, column, 'error', line) output = formatter.show_source(error) + assert output _, pointer = output.rsplit('\n', 1) assert pointer.count(' ') == (column - 1) diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index 0d1f903..2b7995d 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -72,8 +72,7 @@ def test_information(system, pyversion, pyimpl): @mock.patch('json.dumps', return_value='{}') def test_print_information_no_plugins(dumps, information, print_mock): """Verify we print and exit only when we have plugins.""" - plugins = [] - option_manager = mock.Mock(registered_plugins=set(plugins)) + option_manager = mock.Mock(registered_plugins=set()) assert debug.print_information( None, None, None, None, option_manager=option_manager, ) is None diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index fd87a45..635c9d8 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -74,11 +74,10 @@ def test_was_selected_selects_errors(select_list, enable_extensions, def test_was_selected_implicitly_selects_errors(): """Verify we detect users implicitly selecting an error.""" - select_list = [] error_code = 'E121' decider = style_guide.DecisionEngine( create_options( - select=select_list, + select=[], extended_default_select=['E'], ), ) diff --git a/tests/unit/test_option.py b/tests/unit/test_option.py index eb4f95d..e931d13 100644 --- a/tests/unit/test_option.py +++ b/tests/unit/test_option.py @@ -75,4 +75,4 @@ def test_config_name_needs_long_option_name(): def test_dest_is_not_overridden(): """Show that we do not override custom destinations.""" opt = manager.Option('-s', '--short', dest='something_not_short') - assert opt.dest == 'something_not_short' + assert opt.dest == 'something_not_short' # type: ignore diff --git a/tests/unit/test_plugin_type_manager.py b/tests/unit/test_plugin_type_manager.py index e1392a4..4174c58 100644 --- a/tests/unit/test_plugin_type_manager.py +++ b/tests/unit/test_plugin_type_manager.py @@ -1,17 +1,10 @@ """Tests for flake8.plugins.manager.PluginTypeManager.""" -import sys - import mock import pytest from flake8 import exceptions from flake8.plugins import manager -if sys.version_info >= (3, 3): - import collections.abc as collections_abc -else: - import collections as collections_abc - TEST_NAMESPACE = "testing.plugin-type-manager" @@ -91,7 +84,7 @@ def test_generate_call_function(): 'method_name', optmanager, ) - assert isinstance(func, collections_abc.Callable) + assert callable(func) assert func(plugin) is optmanager @@ -168,15 +161,14 @@ def test_provide_options(PluginManager): # noqa: N803 PluginManager.return_value = create_mapping_manager_mock(plugins) optmanager = object() options = object() - extra_args = [] type_mgr = FakeTestType() - type_mgr.provide_options(optmanager, options, extra_args) + type_mgr.provide_options(optmanager, options, []) for plugin in plugins: plugin.provide_options.assert_called_with(optmanager, options, - extra_args) + []) @mock.patch('flake8.plugins.manager.PluginManager') diff --git a/tests/unit/test_statistics.py b/tests/unit/test_statistics.py index 31d6276..16896f0 100644 --- a/tests/unit/test_statistics.py +++ b/tests/unit/test_statistics.py @@ -82,7 +82,7 @@ def test_recording_statistics(): assert isinstance(key, stats.Key) assert isinstance(value, stats.Statistic) - assert storage[(DEFAULT_FILENAME, DEFAULT_ERROR_CODE)].count == 1 + assert storage[stats.Key(DEFAULT_FILENAME, DEFAULT_ERROR_CODE)].count == 1 def test_statistics_for_single_record(): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index dcbf8b8..9f6925e 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -163,12 +163,6 @@ def test_fnmatch(filename, patterns, expected): assert utils.fnmatch(filename, patterns) is expected -def test_fnmatch_returns_the_default_with_empty_default(): - """The default parameter should be returned when no patterns are given.""" - sentinel = object() - assert utils.fnmatch('file.py', [], default=sentinel) is sentinel - - def test_filenames_from_a_directory(): """Verify that filenames_from walks a directory.""" filenames = list(utils.filenames_from('src/flake8/')) @@ -70,13 +70,12 @@ deps = commands = doc8 docs/source/ -[testenv:mypy] +[testenv:pre-commit] basepython = python3 skip_install = true -deps = - mypy +deps = pre-commit commands = - mypy src + pre-commit run --all-files --show-diff-on-failure [testenv:bandit] basepython = python3 |
