Merge branch 'master' into 'master'

Master

Closes #601

See merge request pycqa/flake8!463
This commit is contained in:
Noorhteen Raja J 2021-01-14 21:00:11 +00:00
commit 4312e681c7
6 changed files with 82 additions and 3 deletions

33
src/flake8/cacher.py Normal file
View file

@ -0,0 +1,33 @@
"""Cache files manager."""
import hashlib
import marshal
import os
class Cacher(object):
"""Class to implement saving/loading cache for given file."""
def __init__(self, file_name, cache_path):
# type: (str, str) -> None
"""Init cache class for the given file."""
self.file_name = file_name
checksum = hashlib.md5(file_name.encode()) # nosec
self.cache_path = os.path.join(cache_path, str(checksum.hexdigest()))
def get(self):
"""Get cached result from the cache_path if available and valid."""
if not os.path.exists(self.cache_path):
return
with open(self.cache_path, "rb") as fr:
mtime, results = marshal.load(fr) # nosec
# if the mtime doesn't change then return cache.
# otherwise it is invalid
current_mtime = os.path.getmtime(self.file_name)
if mtime == current_mtime:
return results
def save(self, results):
"""Save the given result to the cache file."""
with open(self.cache_path, "wb") as fw:
current_mtime = os.path.getmtime(self.file_name)
marshal.dump((current_mtime, results), fw)

View file

@ -13,6 +13,7 @@ try:
except ImportError: except ImportError:
multiprocessing = None # type: ignore multiprocessing = None # type: ignore
from flake8 import cacher
from flake8 import defaults from flake8 import defaults
from flake8 import exceptions from flake8 import exceptions
from flake8 import processor from flake8 import processor
@ -360,6 +361,7 @@ class FileChecker(object):
self.options = options self.options = options
self.filename = filename self.filename = filename
self.checks = checks self.checks = checks
self.cacher = cacher.Cacher(filename, self.options.cache_location)
# fmt: off # fmt: off
self.results = [] # type: List[Tuple[str, int, int, str, Optional[str]]] # noqa: E501 self.results = [] # type: List[Tuple[str, int, int, str, Optional[str]]] # noqa: E501
# fmt: on # fmt: on
@ -583,6 +585,22 @@ class FileChecker(object):
self.run_logical_checks() self.run_logical_checks()
def run_checks(self): def run_checks(self):
"""Cache wrapper of self.run_checks."""
# handle both cases where cache is enabled/disabled or invalid
cache_available = False
if self.options.cache:
saved = self.cacher.get()
if saved is not None:
self.filename, self.results, self.statistics = saved
cache_available = True
if cache_available is False:
result = self._run_checks()
if self.options.cache:
self.cacher.save(result)
return self.filename, self.results, self.statistics
def _run_checks(self):
"""Run checks against the file.""" """Run checks against the file."""
try: try:
self.process_tokens() self.process_tokens()

View file

@ -3,6 +3,7 @@ from __future__ import print_function
import argparse import argparse
import logging import logging
import os
import sys import sys
import time import time
from typing import Dict, List, Optional, Set, Tuple from typing import Dict, List, Optional, Set, Tuple
@ -336,6 +337,9 @@ class Application(object):
self.make_formatter() self.make_formatter()
self.make_guide() self.make_guide()
self.make_file_checker_manager() self.make_file_checker_manager()
if self.options.cache:
if not os.path.exists(self.options.cache_location):
os.makedirs(self.options.cache_location)
def report(self): def report(self):
"""Report errors, statistics, and benchmarks.""" """Report errors, statistics, and benchmarks."""

View file

@ -298,6 +298,21 @@ def register_default_options(option_manager):
) )
# Flake8 options # Flake8 options
add_option(
"--cache",
default=False,
parse_from_config=False,
action="store_true",
help="Cache flake8 results on consecutive runs.",
)
add_option(
"--cache-location",
default=".cache/flake8",
parse_from_config=True,
help="Location to store cached results.",
)
add_option( add_option(
"--enable-extensions", "--enable-extensions",
default="", default="",

View file

@ -12,6 +12,7 @@ def options_from(**kwargs):
kwargs.setdefault('verbose', False) kwargs.setdefault('verbose', False)
kwargs.setdefault('stdin_display_name', 'stdin') kwargs.setdefault('stdin_display_name', 'stdin')
kwargs.setdefault('disable_noqa', False) kwargs.setdefault('disable_noqa', False)
kwargs.setdefault('cache_location', ".cache/flake8")
return argparse.Namespace(**kwargs) return argparse.Namespace(**kwargs)

View file

@ -1,4 +1,6 @@
"""Unit tests for the FileChecker class.""" """Unit tests for the FileChecker class."""
import argparse
import mock import mock
import pytest import pytest
@ -6,6 +8,12 @@ import flake8
from flake8 import checker from flake8 import checker
def options(**kwargs):
"""Generate argparse.Namespace for our Application."""
kwargs.setdefault('cache_location', ".cache/flake8")
return argparse.Namespace(**kwargs)
@mock.patch('flake8.processor.FileProcessor') @mock.patch('flake8.processor.FileProcessor')
def test_run_ast_checks_handles_SyntaxErrors(FileProcessor): # noqa: N802,N803 def test_run_ast_checks_handles_SyntaxErrors(FileProcessor): # noqa: N802,N803
"""Stress our SyntaxError handling. """Stress our SyntaxError handling.
@ -16,7 +24,7 @@ def test_run_ast_checks_handles_SyntaxErrors(FileProcessor): # noqa: N802,N803
FileProcessor.return_value = processor FileProcessor.return_value = processor
processor.build_ast.side_effect = SyntaxError('Failed to build ast', processor.build_ast.side_effect = SyntaxError('Failed to build ast',
('', 1, 5, 'foo(\n')) ('', 1, 5, 'foo(\n'))
file_checker = checker.FileChecker(__file__, checks={}, options=object()) file_checker = checker.FileChecker(__file__, checks={}, options=options())
with mock.patch.object(file_checker, 'report') as report: with mock.patch.object(file_checker, 'report') as report:
file_checker.run_ast_checks() file_checker.run_ast_checks()
@ -31,14 +39,14 @@ def test_run_ast_checks_handles_SyntaxErrors(FileProcessor): # noqa: N802,N803
def test_repr(*args): def test_repr(*args):
"""Verify we generate a correct repr.""" """Verify we generate a correct repr."""
file_checker = checker.FileChecker( file_checker = checker.FileChecker(
'example.py', checks={}, options=object(), 'example.py', checks={}, options=options(),
) )
assert repr(file_checker) == 'FileChecker for example.py' assert repr(file_checker) == 'FileChecker for example.py'
def test_nonexistent_file(): def test_nonexistent_file():
"""Verify that checking non-existent file results in an error.""" """Verify that checking non-existent file results in an error."""
c = checker.FileChecker("foobar.py", checks={}, options=object()) c = checker.FileChecker("foobar.py", checks={}, options=options())
assert c.processor is None assert c.processor is None
assert not c.should_process assert not c.should_process