From d666c40623195c633c4fc27869074d82b9d66efd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Oct 2021 05:54:21 -0700 Subject: [PATCH 001/257] remove indent_size_str this was originally added only for pycodestyle which no longer uses it --- src/flake8/processor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 6b7d3c4..f632388 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -92,8 +92,6 @@ class FileProcessor: self.indent_level = 0 #: Number of spaces used for indentation self.indent_size = options.indent_size - #: String representing the space indentation (DEPRECATED) - self.indent_size_str = str(self.indent_size) #: Line number in the file self.line_number = 0 #: Current logical line From ac2059d1547f301e840962715850f1d326c6ae1e Mon Sep 17 00:00:00 2001 From: Guilhem Saurel Date: Mon, 11 Oct 2021 20:49:57 +0200 Subject: [PATCH 002/257] update doc after #1404 --- docs/source/user/configuration.rst | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/docs/source/user/configuration.rst b/docs/source/user/configuration.rst index 31a3e1e..67aba82 100644 --- a/docs/source/user/configuration.rst +++ b/docs/source/user/configuration.rst @@ -36,26 +36,6 @@ there are the defaults. However, there are additional command line options which can alter this. -"User" Configuration --------------------- - -|Flake8| allows a user to use "global" configuration file to store preferences. -The user configuration file is expected to be stored somewhere in the user's -"home" directory. - -- On Windows the "home" directory will be something like - ``C:\\Users\sigmavirus24``, a.k.a, ``~\``. - -- On Linux and other Unix like systems (including OS X) we will look in - ``~/``. - -Note that |Flake8| looks for ``~\.flake8`` on Windows and ``~/.config/flake8`` -on Linux and other Unix systems. - -User configuration files use the same syntax as Project Configuration files. -Keep reading to see that syntax. - - Project Configuration --------------------- From d835bc839703b45c4fb2d4e0b951b49c7e3bb962 Mon Sep 17 00:00:00 2001 From: Guilhem Saurel Date: Mon, 11 Oct 2021 20:54:47 +0200 Subject: [PATCH 003/257] remove more mentions of user-level configuration in docs --- docs/source/user/configuration.rst | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/source/user/configuration.rst b/docs/source/user/configuration.rst index 67aba82..f26674b 100644 --- a/docs/source/user/configuration.rst +++ b/docs/source/user/configuration.rst @@ -24,16 +24,12 @@ Remember that you want to specify certain options without writing Configuration Locations ======================= -|Flake8| supports storing its configuration in the following places: - -- Your top-level user directory - -- In your project in one of ``setup.cfg``, ``tox.ini``, or ``.flake8``. +|Flake8| supports storing its configuration in your project in one of +``setup.cfg``, ``tox.ini``, or ``.flake8``. Values set at the command line have highest priority, then those in the -project configuration file, then those in your user directory, and finally -there are the defaults. However, there are additional command line options -which can alter this. +project configuration file, and finally there are the defaults. However, +there are additional command line options which can alter this. Project Configuration From 8daf52a42abc71468706a93b06f743406bc5dd2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:25:29 +0000 Subject: [PATCH 004/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.910 → v0.910-1](https://github.com/pre-commit/mirrors-mypy/compare/v0.910...v0.910-1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a3a9734..6d6aa62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.910-1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 56dd10eae8300b6f98581fe9700b725f42979955 Mon Sep 17 00:00:00 2001 From: Max R Date: Sun, 17 Oct 2021 21:43:17 -0400 Subject: [PATCH 005/257] Update type hint and expression for _multiprocessing_is_fork() --- src/flake8/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 14c122b..f323442 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -40,9 +40,9 @@ SERIAL_RETRY_ERRNOS = { } -def _multiprocessing_is_fork(): # type () -> bool +def _multiprocessing_is_fork() -> bool: """Class state is only preserved when using the `fork` strategy.""" - return multiprocessing and multiprocessing.get_start_method() == "fork" + return bool(multiprocessing and multiprocessing.get_start_method() == "fork") class Manager: From d58af9f6aeb454e998ad87926c75ef85618bae5d Mon Sep 17 00:00:00 2001 From: Max R Date: Sun, 17 Oct 2021 21:28:22 -0400 Subject: [PATCH 006/257] Remove usage of self.manager.map() in load_plugins() --- src/flake8/plugins/manager.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/flake8/plugins/manager.py b/src/flake8/plugins/manager.py index 6f32e1f..840bf65 100644 --- a/src/flake8/plugins/manager.py +++ b/src/flake8/plugins/manager.py @@ -415,14 +415,11 @@ class PluginTypeManager: if self.plugins_loaded: return - def load_plugin(plugin): - """Call each plugin's load_plugin method.""" - return plugin.load_plugin() + for plugin in self.plugins.values(): + plugin.load_plugin() - plugins = list(self.manager.map(load_plugin)) # Do not set plugins_loaded if we run into an exception self.plugins_loaded = True - return plugins def register_plugin_versions(self, optmanager): """Register the plugins and their versions with the OptionManager.""" From d83b74b6f45e69ddeb455df5e8a70105229cf407 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Oct 2021 01:56:38 +0000 Subject: [PATCH 007/257] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/flake8/checker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index f323442..d008b98 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -42,7 +42,9 @@ SERIAL_RETRY_ERRNOS = { def _multiprocessing_is_fork() -> bool: """Class state is only preserved when using the `fork` strategy.""" - return bool(multiprocessing and multiprocessing.get_start_method() == "fork") + return bool( + multiprocessing and multiprocessing.get_start_method() == "fork" + ) class Manager: From dd6d61c9a6b1001b1ffa5c6c3bc04fbfc35d6e03 Mon Sep 17 00:00:00 2001 From: Max R Date: Sun, 17 Oct 2021 22:13:11 -0400 Subject: [PATCH 008/257] Fix tests --- tests/unit/test_plugin_type_manager.py | 69 +++++++------------------- 1 file changed, 18 insertions(+), 51 deletions(-) diff --git a/tests/unit/test_plugin_type_manager.py b/tests/unit/test_plugin_type_manager.py index 1b823af..ed4fa8c 100644 --- a/tests/unit/test_plugin_type_manager.py +++ b/tests/unit/test_plugin_type_manager.py @@ -24,13 +24,15 @@ def create_mapping_manager_mock(plugins): """Create a mock for the PluginManager.""" # Have a function that will actually call the method underneath def fake_map(func): - for plugin in plugins: + for plugin in plugins.values(): yield func(plugin) # Mock out the PluginManager instance manager_mock = mock.Mock(spec=["map"]) # Replace the map method manager_mock.map = fake_map + # Store the plugins + manager_mock.plugins = plugins return manager_mock @@ -83,24 +85,15 @@ def test_generate_call_function(): def test_load_plugins(PluginManager): # noqa: N803 """Verify load plugins loads *every* plugin.""" # Create a bunch of fake plugins - plugins = [ - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - ] + plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} # Return our PluginManager mock - PluginManager.return_value = create_mapping_manager_mock(plugins) + PluginManager.return_value.plugins = plugins type_mgr = FakeTestType() - # Load the tests (do what we're actually testing) - assert len(type_mgr.load_plugins()) == 8 + # Load the plugins (do what we're actually testing) + type_mgr.load_plugins() # Assert that our closure does what we think it does - for plugin in plugins: + for plugin in plugins.values(): plugin.load_plugin.assert_called_once_with() assert type_mgr.plugins_loaded is True @@ -108,18 +101,10 @@ def test_load_plugins(PluginManager): # noqa: N803 @mock.patch("flake8.plugins.manager.PluginManager") def test_load_plugins_fails(PluginManager): # noqa: N803 """Verify load plugins bubbles up exceptions.""" - plugins = [ - create_plugin_mock(), - create_plugin_mock(True), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - ] + plugins_list = [create_plugin_mock(i == 1) for i in range(8)] + plugins = {"T10%i" % i: plugin for i, plugin in enumerate(plugins_list)} # Return our PluginManager mock - PluginManager.return_value = create_mapping_manager_mock(plugins) + PluginManager.return_value.plugins = plugins type_mgr = FakeTestType() with pytest.raises(exceptions.FailedToLoadPlugin): @@ -128,26 +113,17 @@ def test_load_plugins_fails(PluginManager): # noqa: N803 # Assert we didn't finish loading plugins assert type_mgr.plugins_loaded is False # Assert the first two plugins had their load_plugin method called - plugins[0].load_plugin.assert_called_once_with() - plugins[1].load_plugin.assert_called_once_with() + plugins_list[0].load_plugin.assert_called_once_with() + plugins_list[1].load_plugin.assert_called_once_with() # Assert the rest of the plugins were not loaded - for plugin in plugins[2:]: + for plugin in plugins_list[2:]: assert plugin.load_plugin.called is False @mock.patch("flake8.plugins.manager.PluginManager") def test_register_options(PluginManager): # noqa: N803 """Test that we map over every plugin to register options.""" - plugins = [ - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - ] + plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} # Return our PluginManager mock PluginManager.return_value = create_mapping_manager_mock(plugins) optmanager = object() @@ -155,23 +131,14 @@ def test_register_options(PluginManager): # noqa: N803 type_mgr = FakeTestType() type_mgr.register_options(optmanager) - for plugin in plugins: + for plugin in plugins.values(): plugin.register_options.assert_called_with(optmanager) @mock.patch("flake8.plugins.manager.PluginManager") def test_provide_options(PluginManager): # noqa: N803 """Test that we map over every plugin to provide parsed options.""" - plugins = [ - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - create_plugin_mock(), - ] + plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} # Return our PluginManager mock PluginManager.return_value = create_mapping_manager_mock(plugins) optmanager = object() @@ -180,7 +147,7 @@ def test_provide_options(PluginManager): # noqa: N803 type_mgr = FakeTestType() type_mgr.provide_options(optmanager, options, []) - for plugin in plugins: + for plugin in plugins.values(): plugin.provide_options.assert_called_with(optmanager, options, []) From 84b9831b9dc91daac5f947415d237f16382140f7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 21:18:24 +0000 Subject: [PATCH 009/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.9b0 → 21.10b0](https://github.com/psf/black/compare/21.9b0...21.10b0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d6aa62..29ceca0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 21.10b0 hooks: - id: black args: [--line-length=79] From 221de710711047c2e65cd1f5e9fcebdd8a115236 Mon Sep 17 00:00:00 2001 From: Ian Stapleton Cordasco Date: Tue, 2 Nov 2021 05:52:28 -0500 Subject: [PATCH 010/257] Fix ReadTheDocs builds docutils 0.18 is busted and needs to be prevented from being installed Related to https://github.com/sphinx-doc/sphinx/issues/9788 Related to https://github.com/readthedocs/readthedocs.org/issues/8616 --- docs/source/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index 984ca54..aa123b8 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -2,3 +2,4 @@ sphinx>=1.3.0,!=3.1.0 sphinx_rtd_theme sphinx-prompt flake8-polyfill +docutils!=0.18 From 848003cc0552bb7a17d472682a4d6889811a18fe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 5 Nov 2021 20:05:53 -0400 Subject: [PATCH 011/257] add --color option --- docs/source/user/options.rst | 31 ++++++++++++ src/flake8/formatting/_windows_color.py | 59 +++++++++++++++++++++++ src/flake8/formatting/base.py | 7 +++ src/flake8/formatting/default.py | 21 +++++++- src/flake8/main/options.py | 9 +++- src/flake8/plugins/manager.py | 6 +++ tests/unit/test_base_formatter.py | 46 ++++++++++++++++++ tests/unit/test_filenameonly_formatter.py | 1 + tests/unit/test_nothing_formatter.py | 1 + tox.ini | 2 + 10 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 src/flake8/formatting/_windows_color.py diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index faeb17e..9cdb0ee 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -40,6 +40,8 @@ Index of Options - :option:`flake8 --quiet` +- :option:`flake8 --color` + - :option:`flake8 --count` - :option:`flake8 --diff` @@ -181,6 +183,35 @@ Options and their Descriptions quiet = 1 +.. option:: --color + + :ref:`Go back to index ` + + Whether to use color in output. Defaults to ``auto``. + + Possible options are ``auto``, ``always``, and ``never``. + + This **can** be specified in config files. + + When color is enabled, the following substitutions are enabled: + + - ``%(bold)s`` + - ``%(black)s`` + - ``%(red)s`` + - ``%(green)s`` + - ``%(yellow)s`` + - ``%(blue)s`` + - ``%(magenta)s`` + - ``%(cyan)s`` + - ``%(white)s`` + - ``%(reset)s`` + + Example config file usage: + + .. code-block:: ini + + color = never + .. option:: --count diff --git a/src/flake8/formatting/_windows_color.py b/src/flake8/formatting/_windows_color.py new file mode 100644 index 0000000..1d2c73f --- /dev/null +++ b/src/flake8/formatting/_windows_color.py @@ -0,0 +1,59 @@ +"""ctypes hackery to enable color processing on windows. + +See: https://github.com/pre-commit/pre-commit/blob/cb40e96/pre_commit/color.py +""" +import sys + +if sys.platform == "win32": # pragma: no cover (windows) + + def _enable() -> None: + from ctypes import POINTER + from ctypes import windll + from ctypes import WinError + from ctypes import WINFUNCTYPE + from ctypes.wintypes import BOOL + from ctypes.wintypes import DWORD + from ctypes.wintypes import HANDLE + + STD_ERROR_HANDLE = -12 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + def bool_errcheck(result, func, args): + if not result: + raise WinError() + return args + + GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( + ("GetStdHandle", windll.kernel32), + ((1, "nStdHandle"),), + ) + + GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( + ("GetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (2, "lpMode")), + ) + GetConsoleMode.errcheck = bool_errcheck + + SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( + ("SetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (1, "dwMode")), + ) + SetConsoleMode.errcheck = bool_errcheck + + # As of Windows 10, the Windows console supports (some) ANSI escape + # sequences, but it needs to be enabled using `SetConsoleMode` first. + # + # More info on the escape sequences supported: + # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx + stderr = GetStdHandle(STD_ERROR_HANDLE) + flags = GetConsoleMode(stderr) + SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + try: + _enable() + except OSError: + terminal_supports_color = False + else: + terminal_supports_color = True +else: # pragma: win32 no cover + terminal_supports_color = True diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index 7919f92..a17cb44 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -8,6 +8,8 @@ from typing import Optional from typing import Tuple from typing import TYPE_CHECKING +from flake8.formatting import _windows_color + if TYPE_CHECKING: from flake8.statistics import Statistics from flake8.style_guide import Violation @@ -51,6 +53,11 @@ class BaseFormatter: self.filename = options.output_file self.output_fd: Optional[IO[str]] = None self.newline = "\n" + self.color = options.color == "always" or ( + options.color == "auto" + and sys.stdout.isatty() + and _windows_color.terminal_supports_color + ) self.after_init() def after_init(self) -> None: diff --git a/src/flake8/formatting/default.py b/src/flake8/formatting/default.py index 0a8e09d..7c8073e 100644 --- a/src/flake8/formatting/default.py +++ b/src/flake8/formatting/default.py @@ -8,6 +8,20 @@ from flake8.formatting import base if TYPE_CHECKING: from flake8.style_guide import Violation +COLORS = { + "bold": "\033[1m", + "black": "\033[30m", + "red": "\033[31m", + "green": "\033[32m", + "yellow": "\033[33m", + "blue": "\033[34m", + "magenta": "\033[35m", + "cyan": "\033[36m", + "white": "\033[37m", + "reset": "\033[m", +} +COLORS_OFF = {k: "" for k in COLORS} + class SimpleFormatter(base.BaseFormatter): """Simple abstraction for Default and Pylint formatter commonality. @@ -39,6 +53,7 @@ class SimpleFormatter(base.BaseFormatter): "path": error.filename, "row": error.line_number, "col": error.column_number, + **(COLORS if self.color else COLORS_OFF), } @@ -49,7 +64,11 @@ class Default(SimpleFormatter): format string. """ - error_format = "%(path)s:%(row)d:%(col)d: %(code)s %(text)s" + error_format = ( + "%(bold)s%(path)s%(reset)s" + "%(cyan)s:%(reset)s%(row)d%(cyan)s:%(reset)s%(col)d%(cyan)s:%(reset)s " + "%(bold)s%(red)s%(code)s%(reset)s %(text)s" + ) def after_init(self) -> None: """Check for a custom format string.""" diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index c35dbc6..2a214a7 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -91,6 +91,7 @@ def register_default_options(option_manager): The default options include: - ``-q``/``--quiet`` + - ``--color`` - ``--count`` - ``--diff`` - ``--exclude`` @@ -118,7 +119,6 @@ def register_default_options(option_manager): """ add_option = option_manager.add_option - # pep8 options add_option( "-q", "--quiet", @@ -128,6 +128,13 @@ def register_default_options(option_manager): help="Report only file names, or nothing. This option is repeatable.", ) + add_option( + "--color", + choices=("auto", "always", "never"), + default="auto", + help="Whether to use color in output. Defaults to `%(default)s`.", + ) + add_option( "--count", action="store_true", diff --git a/src/flake8/plugins/manager.py b/src/flake8/plugins/manager.py index 840bf65..d2b9187 100644 --- a/src/flake8/plugins/manager.py +++ b/src/flake8/plugins/manager.py @@ -269,6 +269,12 @@ class PluginManager: # pylint: disable=too-few-public-methods "flake8>=3.7 (which implements per-file-ignores itself)." ) continue + elif entry_point.name == "flake8-colors": + LOG.warning( + "flake8-colors plugin is incompatible with " + "flake8>=4.1 (which implements colors itself)." + ) + continue self._load_plugin_from_entrypoint(entry_point) def _load_plugin_from_entrypoint(self, entry_point, local=False): diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py index 8958903..d096457 100644 --- a/tests/unit/test_base_formatter.py +++ b/tests/unit/test_base_formatter.py @@ -1,15 +1,18 @@ """Tests for the BaseFormatter object.""" import argparse +import sys from unittest import mock import pytest from flake8 import style_guide +from flake8.formatting import _windows_color from flake8.formatting import base def options(**kwargs): """Create an argparse.Namespace instance.""" + kwargs.setdefault("color", "auto") kwargs.setdefault("output_file", None) kwargs.setdefault("tee", False) return argparse.Namespace(**kwargs) @@ -136,6 +139,49 @@ def test_write_produces_stdout(capsys): assert capsys.readouterr().out == f"{line}\n{source}\n" +def test_color_always_is_true(): + """Verify that color='always' sets it to True.""" + formatter = base.BaseFormatter(options(color="always")) + assert formatter.color is True + + +def _mock_isatty(val): + attrs = {"isatty.return_value": val} + return mock.patch.object(sys, "stdout", **attrs) + + +def _mock_windows_color(val): + return mock.patch.object(_windows_color, "terminal_supports_color", val) + + +def test_color_auto_is_true_for_tty(): + """Verify that color='auto' sets it to True for a tty.""" + with _mock_isatty(True), _mock_windows_color(True): + formatter = base.BaseFormatter(options(color="auto")) + assert formatter.color is True + + +def test_color_auto_is_false_without_tty(): + """Verify that color='auto' sets it to False without a tty.""" + with _mock_isatty(False), _mock_windows_color(True): + formatter = base.BaseFormatter(options(color="auto")) + assert formatter.color is False + + +def test_color_auto_is_false_if_not_supported_on_windows(): + """Verify that color='auto' is False if not supported on windows.""" + with _mock_isatty(True), _mock_windows_color(False): + formatter = base.BaseFormatter(options(color="auto")) + assert formatter.color is False + + +def test_color_never_is_false(): + """Verify that color='never' sets it to False despite a tty.""" + with _mock_isatty(True), _mock_windows_color(True): + formatter = base.BaseFormatter(options(color="never")) + assert formatter.color is False + + class AfterInitFormatter(base.BaseFormatter): """Subclass for testing after_init.""" diff --git a/tests/unit/test_filenameonly_formatter.py b/tests/unit/test_filenameonly_formatter.py index 7dda50b..165ef69 100644 --- a/tests/unit/test_filenameonly_formatter.py +++ b/tests/unit/test_filenameonly_formatter.py @@ -7,6 +7,7 @@ from flake8.formatting import default def options(**kwargs): """Create an argparse.Namespace instance.""" + kwargs.setdefault("color", "auto") kwargs.setdefault("output_file", None) kwargs.setdefault("tee", False) return argparse.Namespace(**kwargs) diff --git a/tests/unit/test_nothing_formatter.py b/tests/unit/test_nothing_formatter.py index d7cbea6..c019bdf 100644 --- a/tests/unit/test_nothing_formatter.py +++ b/tests/unit/test_nothing_formatter.py @@ -7,6 +7,7 @@ from flake8.formatting import default def options(**kwargs): """Create an argparse.Namespace instance.""" + kwargs.setdefault("color", "auto") kwargs.setdefault("output_file", None) kwargs.setdefault("tee", False) return argparse.Namespace(**kwargs) diff --git a/tox.ini b/tox.ini index 246f914..5b90a6d 100644 --- a/tox.ini +++ b/tox.ini @@ -127,6 +127,8 @@ commands = # Once Flake8 3.0 is released and in a good state, we can use both and it will # work well \o/ ignore = D203, W503, E203, N818 +per-file-ignores = + src/flake8/formatting/_windows_color.py: N806 exclude = .tox, .git, From e6579239af8b37e6cae2f515609d3ca56573e4b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 5 Nov 2021 20:49:19 -0400 Subject: [PATCH 012/257] deprecate the --diff option --- docs/source/user/options.rst | 5 +++++ src/flake8/main/application.py | 4 ++++ src/flake8/main/options.py | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index 9cdb0ee..e82b0b6 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -238,6 +238,11 @@ Options and their Descriptions :ref:`Go back to index ` + .. warning:: + + Due to hiding potential errors, this option is deprecated and will be + removed in a future version. + Use the unified diff provided on standard in to only check the modified files and report errors included in the diff. diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 44a5524..76c8078 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -191,6 +191,10 @@ class Application: self.running_against_diff = self.options.diff if self.running_against_diff: + LOG.warning( + "the --diff option is deprecated and will be removed in a " + "future version." + ) self.parsed_diff = utils.parse_unified_diff() if not self.parsed_diff: self.exit() diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 2a214a7..470f430 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -146,8 +146,8 @@ def register_default_options(option_manager): add_option( "--diff", action="store_true", - help="Report changes only within line number ranges in the unified " - "diff provided on standard in by the user.", + help="(DEPRECATED) Report changes only within line number ranges in " + "the unified diff provided on standard in by the user.", ) add_option( From 163814547db98dcd26f537a5cba44277558aa6e1 Mon Sep 17 00:00:00 2001 From: Scott Barlow <31610422+Spectre5@users.noreply.github.com> Date: Sun, 7 Nov 2021 17:39:16 -0800 Subject: [PATCH 013/257] Add .nox to default exclude list --- docs/source/manpage.rst | 3 ++- docs/source/user/invocation.rst | 3 ++- docs/source/user/options.rst | 2 +- src/flake8/defaults.py | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/manpage.rst b/docs/source/manpage.rst index 9636b1f..aa3e7b4 100644 --- a/docs/source/manpage.rst +++ b/docs/source/manpage.rst @@ -44,7 +44,8 @@ All options available as of Flake8 3.1.0:: unified diff provided on standard in by the user. --exclude=patterns Comma-separated list of files or directories to exclude. (Default: - .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg) + .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.nox,.eggs, + *.egg) --filename=patterns Only check for filenames matching the patterns in this comma-separated list. (Default: *.py) --stdin-display-name=STDIN_DISPLAY_NAME diff --git a/docs/source/user/invocation.rst b/docs/source/user/invocation.rst index 170a62a..590bcf8 100644 --- a/docs/source/user/invocation.rst +++ b/docs/source/user/invocation.rst @@ -98,7 +98,8 @@ And you should see something like: unified diff provided on standard in by the user. --exclude=patterns Comma-separated list of files or directories to exclude.(Default: - .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg) + .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.nox,.eggs, + *.egg) --filename=patterns Only check for filenames matching the patterns in this comma-separated list. (Default: *.py) --format=format Format errors according to the chosen formatter. diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index e82b0b6..8be702c 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -261,7 +261,7 @@ Options and their Descriptions Provide a comma-separated list of glob patterns to exclude from checks. - This defaults to: ``.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg`` + This defaults to: ``.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.nox,.eggs,*.egg`` Example patterns: diff --git a/src/flake8/defaults.py b/src/flake8/defaults.py index d590857..da0205e 100644 --- a/src/flake8/defaults.py +++ b/src/flake8/defaults.py @@ -9,6 +9,7 @@ EXCLUDE = ( ".git", "__pycache__", ".tox", + ".nox", ".eggs", "*.egg", ) From ef3585b3a04fedfaca608f6c0e84530e1469bbbe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:14:36 -0800 Subject: [PATCH 014/257] require coverage 6.x and simplify config --- .coveragerc | 7 ------- tox.ini | 3 +-- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.coveragerc b/.coveragerc index d1b8806..d74ca19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,4 @@ [run] -parallel = True branch = True source = flake8 @@ -8,12 +7,6 @@ omit = # Don't complain if non-runnable code isn't run */__main__.py -[paths] -source = - src/flake8 - .tox/*/lib/python*/site-packages/flake8 - .tox/pypy/site-packages/flake8 - [report] show_missing = True skip_covered = True diff --git a/tox.ini b/tox.ini index 5b90a6d..63a6ecb 100644 --- a/tox.ini +++ b/tox.ini @@ -5,10 +5,9 @@ envlist = py36,py37,py38,flake8,linters,docs [testenv] deps = pytest!=3.0.5,!=5.2.3 - coverage + coverage>=6 commands = coverage run -m pytest {posargs} - coverage combine coverage report # ensure 100% coverage of tests coverage report --fail-under 100 --include tests/* From 7e5d6501c12aae8ba1126c4c39337ca27c92dd74 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:17:04 -0800 Subject: [PATCH 015/257] remove mentions of flake8-polyfill --- .../cross-compatibility.rst | 187 ------------------ docs/source/plugin-development/index.rst | 1 - docs/source/requirements.txt | 1 - 3 files changed, 189 deletions(-) delete mode 100644 docs/source/plugin-development/cross-compatibility.rst diff --git a/docs/source/plugin-development/cross-compatibility.rst b/docs/source/plugin-development/cross-compatibility.rst deleted file mode 100644 index 07c84e3..0000000 --- a/docs/source/plugin-development/cross-compatibility.rst +++ /dev/null @@ -1,187 +0,0 @@ -==================================== - Writing Plugins For Flake8 2 and 3 -==================================== - -Plugins have existed for |Flake8| 2.x for a few years. There are a number of -these on PyPI already. While it did not seem reasonable for |Flake8| to attempt -to provide a backwards compatible shim for them, we did decide to try to -document the easiest way to write a plugin that's compatible across both -versions. - -.. note:: - - If your plugin does not register options, it *should* Just Work. - -The **only two** breaking changes in |Flake8| 3.0 is the fact that we no -longer check the option parser for a list of strings to parse from a config -file and we no longer patch pep8 or pycodestyle's ``stdin_get_value`` -functions. On |Flake8| 2.x, to have an option parsed from the configuration -files that |Flake8| finds and parses you would have to do something like: - -.. code-block:: python - - parser.add_option('-X', '--example-flag', type='string', - help='...') - parser.config_options.append('example-flag') - -For |Flake8| 3.0, we have added *three* arguments to the -:meth:`~flake8.options.manager.OptionManager.add_option` method you will call -on the parser you receive: - -- ``parse_from_config`` which expects ``True`` or ``False`` - - When ``True``, |Flake8| will parse the option from the config files |Flake8| - finds. - -- ``comma_separated_list`` which expects ``True`` or ``False`` - - When ``True``, |Flake8| will split the string intelligently and handle - extra whitespace. The parsed value will be a list. - -- ``normalize_paths`` which expects ``True`` or ``False`` - - When ``True``, |Flake8| will: - - * remove trailing path separators (i.e., ``os.path.sep``) - - * return the absolute path for values that have the separator in them - -All three of these options can be combined or used separately. - - -Parsing Options from Configuration Files -======================================== - -The example from |Flake8| 2.x now looks like: - -.. code-block:: python - - parser.add_option('-X', '--example-flag', type='string', - parse_from_config=True, - help='...') - - -Parsing Comma-Separated Lists -============================= - -Now let's imagine that the option we want to add is expecting a comma-separatd -list of values from the user (e.g., ``--select E123,W503,F405``). |Flake8| 2.x -often forced users to parse these lists themselves since pep8 special-cased -certain flags and left others on their own. |Flake8| 3.0 adds -``comma_separated_list`` so that the parsed option is already a list for -plugin authors. When combined with ``parse_from_config`` this means that users -can also do something like: - -.. code-block:: ini - - example-flag = - first, - second, - third, - fourth, - fifth - -And |Flake8| will just return the list: - -.. code-block:: python - - ["first", "second", "third", "fourth", "fifth"] - - -Normalizing Values that Are Paths -================================= - -Finally, let's imagine that our new option wants a path or list of paths. To -ensure that these paths are semi-normalized (the way |Flake8| 2.x used to -work) we need only pass ``normalize_paths=True``. If you have specified -``comma_separated_list=True`` then this will parse the value as a list of -paths that have been normalized. Otherwise, this will parse the value -as a single path. - - -Option Handling on Flake8 2 and 3 -================================= - -To ease the transition, the |Flake8| maintainers have released -`flake8-polyfill`_. |polyfill| provides a convenience function to help users -transition between Flake8 2 and 3 without issue. For example, if your plugin -has to work on Flake8 2.x and 3.x but you want to take advantage of some of -the new options to ``add_option``, you can do - -.. code-block:: python - - from flake8_polyfill import options - - - class MyPlugin(object): - @classmethod - def add_options(cls, parser): - options.register( - parser, - '--application-names', default='', type='string', - help='Names of the applications to be checked.', - parse_from_config=True, - comma_separated_list=True, - ) - options.register( - parser, - '--style-name', default='', type='string', - help='The name of the style convention you want to use', - parse_from_config=True, - ) - options.register( - parser, - '--application-paths', default='', type='string', - help='Locations of the application code', - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - ) - - @classmethod - def parse_options(cls, parsed_options): - cls.application_names = parsed_options.application_names - cls.style_name = parsed_options.style_name - cls.application_paths = parsed_options.application_paths - -|polyfill| will handle these extra options using *callbacks* to the option -parser. The project has direct replications of the functions that |Flake8| -uses to provide the same functionality. This means that the values you receive -should be identically parsed whether you're using Flake8 2.x or 3.x. - -.. autofunction:: flake8_polyfill.options.register - - -Standard In Handling on Flake8 2.5, 2.6, and 3 -============================================== - -After releasing |Flake8| 2.6, handling standard-in became a bit trickier for -some plugins. |Flake8| 2.5 and earlier had started monkey-patching pep8's -``stdin_get_value`` function. 2.6 switched to pycodestyle and only -monkey-patched that. 3.0 has its own internal implementation and uses that but -does not directly provide anything for plugins using pep8 and pycodestyle's -``stdin_get_value`` function. |polyfill| provides this functionality for -plugin developers via its :mod:`flake8_polyfill.stdin` module. - -If a plugin needs to read the content from stdin, it can do the following: - -.. code-block:: python - - from flake8_polyfill import stdin - - stdin.monkey_patch('pep8') # To monkey-patch only pep8 - stdin.monkey_patch('pycodestyle') # To monkey-patch only pycodestyle - stdin.monkey_patch('all') # To monkey-patch both pep8 and pycodestyle - - -Further, when using ``all``, |polyfill| does not require both packages to be -installed but will attempt to monkey-patch both and will silently ignore the -fact that pep8 or pycodestyle is not installed. - -.. autofunction:: flake8_polyfill.stdin.monkey_patch - - -.. links -.. _flake8-polyfill: https://pypi.org/project/flake8-polyfill/ - -.. |polyfill| replace:: ``flake8-polyfill`` diff --git a/docs/source/plugin-development/index.rst b/docs/source/plugin-development/index.rst index 241e49f..c89e5f0 100644 --- a/docs/source/plugin-development/index.rst +++ b/docs/source/plugin-development/index.rst @@ -62,7 +62,6 @@ Here's a tutorial which goes over building an ast checking plugin from scratch: registering-plugins plugin-parameters formatters - cross-compatibility .. _entry points: diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index aa123b8..cfcfbdf 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1,5 +1,4 @@ sphinx>=1.3.0,!=3.1.0 sphinx_rtd_theme sphinx-prompt -flake8-polyfill docutils!=0.18 From 5319cf9f35c4977075f40262af2751d58c96b3d4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:18:14 -0800 Subject: [PATCH 016/257] delete unused codecov.yml --- codecov.yml | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 475c4c5..0000000 --- a/codecov.yml +++ /dev/null @@ -1,42 +0,0 @@ -codecov: - branch: main - bot: null - -coverage: - precision: 2 - round: down - range: "60...100" - - notify: - irc: - default: - server: chat.freenode.net - channel: '##python-code-quality' - branches: main - threshold: 2 - message: null - - status: - project: - default: - target: auto - threshold: null - branches: null - - patch: - default: - target: auto - branches: null - - changes: - default: - branches: null - - ignore: null - fixes: - - .tox - -comment: - layout: "header, diff, changes, sunburst, uncovered, tree" - branches: null - behavior: default From ba8f093565f444ba2506fecfa14ccd4f2b17a2a0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:19:29 -0800 Subject: [PATCH 017/257] remove mercurial tags/ignore files --- .hgignore | 13 ------------- .hgtags | 31 ------------------------------- 2 files changed, 44 deletions(-) delete mode 100644 .hgignore delete mode 100644 .hgtags diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 65d5044..0000000 --- a/.hgignore +++ /dev/null @@ -1,13 +0,0 @@ -^bin/ -^dist/ -^include/ -^lib/ -^man/ -^\.tox/ -/__pycache__/ -\.egg$ -\.egg-info/ -\.Python -\.orig$ -\.pyc$ -\.swp$ diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 1e09236..0000000 --- a/.hgtags +++ /dev/null @@ -1,31 +0,0 @@ -72a440d1fa2ddd69590cd71a51509a653fcdc235 0.1 -4115a858709a3b625d3f7ebbc14dc77182052235 0.2 -874fdfd29b704f1131420cc9f0d226c929fe255f 0.3 -28547d53010248c55e77678087a7ef4195ab7a8a 0.4 -ea4d5abfd0c02a96dd10cfe7ec3e7a76a080f38f 0.5 -a914ae72b4eae171ca50b0ebda12765a68c7f744 0.6 -405ffe2bfa854ba85428ee1627f5e0b01246ee22 0.7 -b04dcad949573b1cef99392cf66caa5c55a0f930 0.8 -6403df46028ffe602f5834743099c5160938aa6b 0.9 -f49f8afba1da337074eb1dbb911ad0ec2a2c6199 1.0 -33104f05ce6c430c27b6414d85e37a88468e6aff 1.1 -575a782a8fb5d42e87954fd0a9253ffae6268023 1.2 -575a782a8fb5d42e87954fd0a9253ffae6268023 1.2 -de690f0eb4029802d6dc67ab7e1760a914d3eb0c 1.2 -0407b6714ca42766edea6f3b17e183cac8fa596b 1.3 -c522d468c5b86329a8b562ca7e392e544a45fffa 1.3.1 -ff671fabec71e85d32395c35c40a125432859e49 1.5 -30bf3a998b09303da032c03d61041180e6ba3d83 1.6 -1184216fb3619680517d3f8386dc138ab2d5ee26 1.6.1 -bca1826148f9ea22a89d9533d19a79ba6678293f 1.6.2 -61b1bc18f258cf2647f4af29c3dfe48d268eeb0b 1.7.0 -374b8e63d93b8743c3dad093bca449e01fdd287f 2.0 -9b641817ffe6be1ff7d34711d203e27c8f3733f8 2.1.0 -cacf6cc4290692456a9164d27b661acfcbdfcd12 2.2.0 -fabf3cf87eafa8826aae91572e20e2e232d310ab 2.2.1 -3f35951906d20667b9f4b67baff0299e637ce611 2.2.2 -3f35951906d20667b9f4b67baff0299e637ce611 2.2.2 -0000000000000000000000000000000000000000 2.2.2 -0000000000000000000000000000000000000000 2.2.2 -9ba7be5e4374230e00dc1203b5b45ab0c67ecc23 2.2.2 -5dc9b776132736d7e11331aafd9d5c36faf6839b 2.2.3 From 5bed7878834d37922966b11b4dd0e4eb2fe83b8f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:30:10 -0800 Subject: [PATCH 018/257] add typing to src/flake8/__init__.py --- setup.cfg | 2 ++ src/flake8/__init__.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 763992b..fca70c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -107,6 +107,8 @@ no_implicit_optional = true warn_unused_ignores = true # TODO: until we opt in all the modules +[mypy-flake8.__init__] +disallow_untyped_defs = true [mypy-flake8.defaults] disallow_untyped_defs = true [mypy-flake8.exceptions] diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 6b40748..53bc407 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -11,6 +11,7 @@ This module """ import logging import sys +from typing import Optional from typing import Type LOG = logging.getLogger(__name__) @@ -40,7 +41,11 @@ LOG_FORMAT = ( ) -def configure_logging(verbosity, filename=None, logformat=LOG_FORMAT): +def configure_logging( + verbosity: int, + filename: Optional[str] = None, + logformat: str = LOG_FORMAT, +) -> None: """Configure logging for flake8. :param int verbosity: From 0c62569c4f75ffcbbe21561bd21c52b6e3021a12 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:36:02 -0800 Subject: [PATCH 019/257] remove dead code detected using https://github.com/asottile/dead --- docs/source/internal/utils.rst | 5 ----- src/flake8/defaults.py | 2 -- src/flake8/options/manager.py | 28 ---------------------------- src/flake8/processor.py | 1 - src/flake8/utils.py | 11 ----------- tests/unit/test_option.py | 8 -------- tests/unit/test_utils.py | 9 --------- 7 files changed, 64 deletions(-) diff --git a/docs/source/internal/utils.rst b/docs/source/internal/utils.rst index e104d62..387ec82 100644 --- a/docs/source/internal/utils.rst +++ b/docs/source/internal/utils.rst @@ -54,11 +54,6 @@ normalized path. This function retrieves and caches the value provided on ``sys.stdin``. This allows plugins to use this to retrieve ``stdin`` if necessary. -.. autofunction:: flake8.utils.is_windows - -This provides a convenient and explicitly named function that checks if we are -currently running on a Windows (or ``nt``) operating system. - .. autofunction:: flake8.utils.is_using_stdin Another helpful function that is named only to be explicit given it is a very diff --git a/src/flake8/defaults.py b/src/flake8/defaults.py index da0205e..a1c04fc 100644 --- a/src/flake8/defaults.py +++ b/src/flake8/defaults.py @@ -18,8 +18,6 @@ SELECT = ("E", "F", "W", "C90") MAX_LINE_LENGTH = 79 INDENT_SIZE = 4 -TRUTHY_VALUES = {"true", "1", "t"} - # Other constants WHITESPACE = frozenset(" \t") diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 28cee4d..50bdf58 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -17,14 +17,10 @@ from typing import Sequence from typing import Set from typing import Tuple from typing import Type -from typing import TYPE_CHECKING from typing import Union from flake8 import utils -if TYPE_CHECKING: - from typing import NoReturn - LOG = logging.getLogger(__name__) # represent a singleton of "not passed arguments". @@ -310,34 +306,10 @@ class Option: return value - def normalize_from_setuptools( - self, value: str - ) -> Union[int, float, complex, bool, str]: - """Normalize the value received from setuptools.""" - value = self.normalize(value) - if self.type is int or self.action == "count": - return int(value) - elif self.type is float: - return float(value) - elif self.type is complex: - return complex(value) - if self.action in ("store_true", "store_false"): - value = str(value).upper() - if value in ("1", "T", "TRUE", "ON"): - return True - if value in ("0", "F", "FALSE", "OFF"): - return False - return value - def to_argparse(self) -> Tuple[List[str], Dict[str, Any]]: """Convert a Flake8 Option to argparse ``add_argument`` arguments.""" return self.option_args, self.filtered_option_kwargs - @property - def to_optparse(self) -> "NoReturn": - """No longer functional.""" - raise AttributeError("to_optparse: flake8 now uses argparse") - PluginVersion = collections.namedtuple( "PluginVersion", ["name", "version", "local"] diff --git a/src/flake8/processor.py b/src/flake8/processor.py index f632388..2649c9d 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -16,7 +16,6 @@ from flake8 import defaults from flake8 import utils LOG = logging.getLogger(__name__) -PyCF_ONLY_AST = 1024 NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE]) SKIP_TOKENS = frozenset( diff --git a/src/flake8/utils.py b/src/flake8/utils.py index 62c8ad4..4e68104 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -281,17 +281,6 @@ def parse_unified_diff(diff: Optional[str] = None) -> Dict[str, Set[int]]: return parsed_paths -def is_windows() -> bool: - """Determine if we're running on Windows. - - :returns: - True if running on Windows, otherwise False - :rtype: - bool - """ - return os.name == "nt" - - def is_using_stdin(paths: List[str]) -> bool: """Determine if we're going to read from stdin. diff --git a/tests/unit/test_option.py b/tests/unit/test_option.py index 52aef27..d576c49 100644 --- a/tests/unit/test_option.py +++ b/tests/unit/test_option.py @@ -25,14 +25,6 @@ def test_to_argparse(): assert isinstance(kwargs["type"], functools.partial) -def test_to_optparse(): - """Test that .to_optparse() produces a useful error message.""" - with pytest.raises(AttributeError) as excinfo: - manager.Option("--foo").to_optparse - (msg,) = excinfo.value.args - assert msg == "to_optparse: flake8 now uses argparse" - - def test_to_argparse_creates_an_option_as_we_expect(): """Show that we pass all keyword args to argparse.""" opt = manager.Option("-t", "--test", action="count") diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 22bb44d..947b67f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -160,15 +160,6 @@ def test_normalize_paths(value, expected): assert utils.normalize_paths(value) == expected -def test_is_windows_checks_for_nt(): - """Verify that we correctly detect Windows.""" - with mock.patch.object(os, "name", "nt"): - assert utils.is_windows() is True - - with mock.patch.object(os, "name", "posix"): - assert utils.is_windows() is False - - @pytest.mark.parametrize( "filename,patterns,expected", [ From b3c6364f0e1671a680679bc3488a154bf24f756a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:42:35 -0800 Subject: [PATCH 020/257] remove unused example-code test fixtures --- .pre-commit-config.yaml | 2 +- tests/fixtures/example-code/empty.py | 0 tests/fixtures/example-code/inline-ignores/E501.py | 4 ---- tests/fixtures/example-code/inline-ignores/E731.py | 1 - tests/fixtures/example-code/invalid-syntax.py | 1 - 5 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 tests/fixtures/example-code/empty.py delete mode 100644 tests/fixtures/example-code/inline-ignores/E501.py delete mode 100644 tests/fixtures/example-code/inline-ignores/E731.py delete mode 100644 tests/fixtures/example-code/invalid-syntax.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29ceca0..bcbb585 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,3 @@ -exclude: ^tests/fixtures/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 @@ -7,6 +6,7 @@ repos: - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace + exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports rev: v2.6.0 hooks: diff --git a/tests/fixtures/example-code/empty.py b/tests/fixtures/example-code/empty.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/example-code/inline-ignores/E501.py b/tests/fixtures/example-code/inline-ignores/E501.py deleted file mode 100644 index 64a5dca..0000000 --- a/tests/fixtures/example-code/inline-ignores/E501.py +++ /dev/null @@ -1,4 +0,0 @@ -from some.module.that.has.nested.sub.modules import \ - ClassWithVeryVeryVeryVeryLongName # noqa: E501,F401 - -# ClassWithVeryVeryVeryVeryLongName() diff --git a/tests/fixtures/example-code/inline-ignores/E731.py b/tests/fixtures/example-code/inline-ignores/E731.py deleted file mode 100644 index 866c79e..0000000 --- a/tests/fixtures/example-code/inline-ignores/E731.py +++ /dev/null @@ -1 +0,0 @@ -example = lambda: 'example' # noqa: E731 diff --git a/tests/fixtures/example-code/invalid-syntax.py b/tests/fixtures/example-code/invalid-syntax.py deleted file mode 100644 index db2cc27..0000000 --- a/tests/fixtures/example-code/invalid-syntax.py +++ /dev/null @@ -1 +0,0 @@ -foo( From e527b8e3cac10d3c61a92e9d28c338d8e99e9e09 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:49:01 -0800 Subject: [PATCH 021/257] simplify our own flake8 config --- docs/source/conf.py | 14 +++++--------- tox.ini | 20 +------------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index f6a4d4e..9f0795b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,13 +10,11 @@ # # All configuration values have a default; values that are commented out # serve to show the default. -import os -import sys - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) +import flake8 # -- General configuration ------------------------------------------------ @@ -56,8 +54,6 @@ project = "flake8" copyright = "2016, Ian Stapleton Cordasco" author = "Ian Stapleton Cordasco" -import flake8 - # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. @@ -218,13 +214,13 @@ htmlhelp_basename = "flake8doc" latex_elements = { # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', + # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', + # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. - #'preamble': '', + # 'preamble': '', # Latex figure (float) alignment - #'figure_align': 'htbp', + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples diff --git a/tox.ini b/tox.ini index 63a6ecb..4f355d8 100644 --- a/tox.ini +++ b/tox.ini @@ -118,26 +118,8 @@ commands = {[testenv:build]commands} twine upload --skip-existing dist/* -# Flake8 Configuration [flake8] -# Ignore some flake8-docstrings errors -# NOTE(sigmavirus24): While we're still using flake8 2.x, this ignore line -# defaults to selecting all other errors so we do not need select=E,F,W,I,D -# Once Flake8 3.0 is released and in a good state, we can use both and it will -# work well \o/ -ignore = D203, W503, E203, N818 +extend-ignore = E203 per-file-ignores = src/flake8/formatting/_windows_color.py: N806 -exclude = - .tox, - .git, - __pycache__, - docs/source/conf.py, - build, - dist, - tests/fixtures/*, - *.pyc, - *.egg-info, - .cache, - .eggs max-complexity = 10 From 3af47b7df54554919f2357b139ec55d2dc639fee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 08:54:57 -0800 Subject: [PATCH 022/257] remove some unused test fixture files --- tests/fixtures/config_files/README.rst | 10 ---------- tests/fixtures/config_files/local-config.ini | 3 --- tests/fixtures/config_files/user-config.ini | 5 ----- 3 files changed, 18 deletions(-) delete mode 100644 tests/fixtures/config_files/local-config.ini delete mode 100644 tests/fixtures/config_files/user-config.ini diff --git a/tests/fixtures/config_files/README.rst b/tests/fixtures/config_files/README.rst index f6994c9..4570989 100644 --- a/tests/fixtures/config_files/README.rst +++ b/tests/fixtures/config_files/README.rst @@ -21,11 +21,6 @@ Purposes of existing fixtures This should only be used when providing config file(s) specified by the user on the command-line. -``tests/fixtures/config_files/local-config.ini`` - - This should be used when providing config files that would have been found - by looking for config files in the current working project directory. - ``tests/fixtures/config_files/local-plugin.ini`` This is for testing configuring a plugin via flake8 config file instead of @@ -35,8 +30,3 @@ Purposes of existing fixtures This should be used when parsing an ini file without a ``[flake8]`` section. - -``tests/fixtures/config_files/user-config.ini`` - - This is an example configuration file that would be found in the user's - home directory (or XDG Configuration Directory). diff --git a/tests/fixtures/config_files/local-config.ini b/tests/fixtures/config_files/local-config.ini deleted file mode 100644 index 348751a..0000000 --- a/tests/fixtures/config_files/local-config.ini +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -exclude = docs/ -select = E,W,F diff --git a/tests/fixtures/config_files/user-config.ini b/tests/fixtures/config_files/user-config.ini deleted file mode 100644 index b06c24f..0000000 --- a/tests/fixtures/config_files/user-config.ini +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -exclude = - tests/fixtures/, - docs/ -ignore = D203 From bb3c8d2607408187feaa56e8ce8e58d6c3676170 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 09:35:49 -0800 Subject: [PATCH 023/257] improve coverage a bit --- src/flake8/checker.py | 17 ++++------------ tests/integration/test_main.py | 23 ++++++++++++++++++++++ tests/unit/test_checker_manager.py | 31 ++++++++++++++++++++++-------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index d008b98..cbff46f 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -3,6 +3,7 @@ import collections import errno import itertools import logging +import multiprocessing.pool import signal import tokenize from typing import Dict @@ -15,11 +16,6 @@ from flake8 import exceptions from flake8 import processor from flake8 import utils -try: - import multiprocessing.pool -except ImportError: - multiprocessing = None # type: ignore - Results = List[Tuple[str, int, int, str, Optional[str]]] LOG = logging.getLogger(__name__) @@ -40,13 +36,6 @@ SERIAL_RETRY_ERRNOS = { } -def _multiprocessing_is_fork() -> bool: - """Class state is only preserved when using the `fork` strategy.""" - return bool( - multiprocessing and multiprocessing.get_start_method() == "fork" - ) - - class Manager: """Manage the parallelism and checker instances for each plugin and file. @@ -113,7 +102,9 @@ class Manager: # - we're processing a diff, which again does not work well with # multiprocessing and which really shouldn't require multiprocessing # - the user provided some awful input - if not _multiprocessing_is_fork(): + + # class state is only preserved when using the `fork` strategy. + if multiprocessing.get_start_method() != "fork": LOG.warning( "The multiprocessing module is not available. " "Ignoring --jobs arguments." diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 8ad7654..6c92c4a 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -227,6 +227,29 @@ def test_bug_report_successful(capsys): assert err == "" +def test_benchmark_successful(tmp_path, capsys): + """Test that --benchmark does not crash.""" + fname = tmp_path.joinpath("t.py") + fname.write_text("print('hello world')\n") + + _call_main(["--benchmark", str(fname)]) + + out, err = capsys.readouterr() + parts = [line.split(maxsplit=1) for line in out.splitlines()] + assert parts == [ + [mock.ANY, "seconds elapsed"], + ["1", "total logical lines processed"], + [mock.ANY, "logical lines processed per second"], + ["1", "total physical lines processed"], + [mock.ANY, "physical lines processed per second"], + ["5", "total tokens processed"], + [mock.ANY, "tokens processed per second"], + ["1", "total files processed"], + [mock.ANY, "files processed per second"], + ] + assert err == "" + + def test_specific_noqa_does_not_clobber_pycodestyle_noqa(tmpdir, capsys): """See https://github.com/pycqa/flake8/issues/1104.""" with tmpdir.as_cwd(): diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index f82dc49..8e45675 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -1,5 +1,6 @@ """Tests for the Manager object for FileCheckers.""" import errno +import multiprocessing from unittest import mock import pytest @@ -37,8 +38,8 @@ def test_oserrors_cause_serial_fall_back(): assert serial.call_count == 1 -@mock.patch("flake8.checker._multiprocessing_is_fork", return_value=True) -def test_oserrors_are_reraised(is_windows): +@mock.patch.object(multiprocessing, "get_start_method", return_value="fork") +def test_oserrors_are_reraised(_): """Verify that unexpected OSErrors will cause the Manager to reraise.""" err = OSError(errno.EAGAIN, "Ominous message") with mock.patch("_multiprocessing.SemLock", side_effect=err): @@ -49,15 +50,30 @@ def test_oserrors_are_reraised(is_windows): assert serial.call_count == 0 -def test_multiprocessing_is_disabled(): +@mock.patch.object(multiprocessing, "get_start_method", return_value="spawn") +def test_multiprocessing_is_disabled(_): """Verify not being able to import multiprocessing forces jobs to 0.""" style_guide = style_guide_mock() - with mock.patch("flake8.checker.multiprocessing", None): + manager = checker.Manager(style_guide, [], []) + assert manager.jobs == 0 + + +def test_multiprocessing_cpu_count_not_implemented(): + """Verify that jobs is 0 if cpu_count is unavailable.""" + style_guide = style_guide_mock() + style_guide.options.jobs = JobsArgument("auto") + + with mock.patch.object( + multiprocessing, + "cpu_count", + side_effect=NotImplementedError, + ): manager = checker.Manager(style_guide, [], []) - assert manager.jobs == 0 + assert manager.jobs == 0 -def test_make_checkers(): +@mock.patch.object(multiprocessing, "get_start_method", return_value="spawn") +def test_make_checkers(_): """Verify that we create a list of FileChecker instances.""" style_guide = style_guide_mock() files = ["file1", "file2"] @@ -67,8 +83,7 @@ def test_make_checkers(): "logical_line_plugins": [], "physical_line_plugins": [], } - with mock.patch("flake8.checker.multiprocessing", None): - manager = checker.Manager(style_guide, files, checkplugins) + manager = checker.Manager(style_guide, files, checkplugins) with mock.patch("flake8.utils.filenames_from") as filenames_from: filenames_from.side_effect = [["file1"], ["file2"]] From 0698366a20a57b579fe2fe67f9c2f668be372274 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 14:42:48 -0800 Subject: [PATCH 024/257] add a __repr__ for JobsArgument --- src/flake8/main/options.py | 4 ++++ tests/unit/test_option_manager.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 470f430..74440db 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -80,6 +80,10 @@ class JobsArgument: f"{arg!r} must be 'auto' or an integer.", ) + def __repr__(self) -> str: + """Representation for debugging.""" + return f"{type(self).__name__}({str(self)!r})" + def __str__(self): """Format our JobsArgument class.""" return "auto" if self.is_auto else str(self.n_jobs) diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index 93f94e9..2eb590a 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -387,3 +387,9 @@ def test_jobs_argument_str(): """Test that JobsArgument has a correct __str__.""" assert str(JobsArgument("auto")) == "auto" assert str(JobsArgument("123")) == "123" + + +def test_jobs_argument_repr(): + """Test that JobsArgument has a correct __repr__.""" + assert repr(JobsArgument("auto")) == "JobsArgument('auto')" + assert repr(JobsArgument("123")) == "JobsArgument('123')" From 8d3afe40e1e8ba70418bada82608d47e61eedb8f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 15:39:14 -0800 Subject: [PATCH 025/257] use return value of parse_args directly --- src/flake8/main/application.py | 12 +++++------- src/flake8/options/aggregator.py | 5 ++--- src/flake8/options/manager.py | 6 ++---- tests/integration/test_aggregator.py | 4 ++-- tests/unit/test_option_manager.py | 20 ++++++++++---------- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 76c8078..c85172c 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -80,9 +80,6 @@ class Application: #: The user-supplied options parsed into an instance of #: :class:`argparse.Namespace` self.options: Optional[argparse.Namespace] = None - #: The left over arguments that were not parsed by - #: :attr:`option_manager` - self.args: Optional[List[str]] = None #: The number of errors, warnings, and other messages after running #: flake8 and taking into account ignored errors and lines. self.result_count = 0 @@ -183,7 +180,7 @@ class Application: :param list argv: Command-line arguments passed in directly. """ - self.options, self.args = aggregator.aggregate_options( + self.options = aggregator.aggregate_options( self.option_manager, config_finder, argv, @@ -201,11 +198,11 @@ class Application: assert self.check_plugins is not None self.check_plugins.provide_options( - self.option_manager, self.options, self.args + self.option_manager, self.options, self.options.filenames ) assert self.formatting_plugins is not None self.formatting_plugins.provide_options( - self.option_manager, self.options, self.args + self.option_manager, self.options, self.options.filenames ) def formatter_for(self, formatter_plugin_name): @@ -251,9 +248,10 @@ class Application: def make_file_checker_manager(self) -> None: """Initialize our FileChecker Manager.""" + assert self.options is not None self.file_checker_manager = checker.Manager( style_guide=self.guide, - arguments=self.args, + arguments=self.options.filenames, checker_plugins=self.check_plugins, ) diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index 73a0f36..6458d69 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -6,7 +6,6 @@ applies the user-specified command-line configuration on top of it. import argparse import logging from typing import List -from typing import Tuple from flake8.options import config from flake8.options.manager import OptionManager @@ -18,7 +17,7 @@ def aggregate_options( manager: OptionManager, config_finder: config.ConfigFileFinder, argv: List[str], -) -> Tuple[argparse.Namespace, List[str]]: +) -> argparse.Namespace: """Aggregate and merge CLI and config file options. :param flake8.options.manager.OptionManager manager: @@ -35,7 +34,7 @@ def aggregate_options( tuple(argparse.Namespace, list) """ # Get defaults from the option parser - default_values, _ = manager.parse_args([]) + default_values = manager.parse_args([]) # Make our new configuration file mergerator config_parser = config.ConfigParser( diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 50bdf58..af6f31c 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -459,15 +459,13 @@ class OptionManager: self, args: Optional[List[str]] = None, values: Optional[argparse.Namespace] = None, - ) -> Tuple[argparse.Namespace, List[str]]: + ) -> argparse.Namespace: """Proxy to calling the OptionParser's parse_args method.""" self.generate_epilog() self.update_version_string() if values: self.parser.set_defaults(**vars(values)) - parsed_args = self.parser.parse_args(args) - # TODO: refactor callers to not need this - return parsed_args, parsed_args.filenames + return self.parser.parse_args(args) def parse_known_args( self, args: Optional[List[str]] = None diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py index ae75204..70331a4 100644 --- a/tests/integration/test_aggregator.py +++ b/tests/integration/test_aggregator.py @@ -38,7 +38,7 @@ def test_aggregate_options_with_config(optmanager): config_finder = config.ConfigFileFinder( "flake8", config_file=CLI_SPECIFIED_CONFIG ) - options, args = aggregator.aggregate_options( + options = aggregator.aggregate_options( optmanager, config_finder, arguments ) @@ -58,7 +58,7 @@ def test_aggregate_options_when_isolated(optmanager): ] config_finder = config.ConfigFileFinder("flake8", ignore_config_files=True) optmanager.extend_default_ignore(["E8"]) - options, args = aggregator.aggregate_options( + options = aggregator.aggregate_options( optmanager, config_finder, arguments ) diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index 2eb590a..0dee442 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -33,16 +33,16 @@ def test_option_manager_including_parent_options(): optmanager = manager.OptionManager( prog="flake8", version=TEST_VERSION, parents=[parent_parser] ) - option, _ = optmanager.parse_args(["--parent", "foo"]) + options = optmanager.parse_args(["--parent", "foo"]) # THEN - assert option.parent == "foo" + assert options.parent == "foo" def test_parse_args_forwarding_default_values(optmanager): """Verify default provided values are present in the final result.""" namespace = argparse.Namespace(foo="bar") - options, args = optmanager.parse_args([], namespace) + options = optmanager.parse_args([], namespace) assert options.foo == "bar" @@ -50,7 +50,7 @@ def test_parse_args_forwarding_type_coercion(optmanager): """Verify default provided values are type converted from add_option.""" optmanager.add_option("--foo", type=int) namespace = argparse.Namespace(foo="5") - options, args = optmanager.parse_args([], namespace) + options = optmanager.parse_args([], namespace) assert options.foo == 5 @@ -104,7 +104,7 @@ def test_parse_args_normalize_path(optmanager): optmanager.add_option("--config", normalize_paths=True) - options, args = optmanager.parse_args(["--config", "../config.ini"]) + options = optmanager.parse_args(["--config", "../config.ini"]) assert options.config == os.path.abspath("../config.ini") @@ -117,7 +117,7 @@ def test_parse_args_handles_comma_separated_defaults(optmanager): "--exclude", default="E123,W234", comma_separated_list=True ) - options, args = optmanager.parse_args([]) + options = optmanager.parse_args([]) assert options.exclude == ["E123", "W234"] @@ -130,7 +130,7 @@ def test_parse_args_handles_comma_separated_lists(optmanager): "--exclude", default="E123,W234", comma_separated_list=True ) - options, args = optmanager.parse_args(["--exclude", "E201,W111,F280"]) + options = optmanager.parse_args(["--exclude", "E201,W111,F280"]) assert options.exclude == ["E201", "W111", "F280"] @@ -143,7 +143,7 @@ def test_parse_args_normalize_paths(optmanager): "--extra-config", normalize_paths=True, comma_separated_list=True ) - options, args = optmanager.parse_args( + options = optmanager.parse_args( ["--extra-config", "../config.ini,tox.ini,flake8/some-other.cfg"] ) assert options.extra_config == [ @@ -319,14 +319,14 @@ def test_optparse_normalize_callback_option_legacy(optmanager): def test_optparse_normalize_types(optmanager, type_s, input_val, expected): """Test the optparse shim for type="typename".""" optmanager.add_option("--foo", type=type_s) - opts, args = optmanager.parse_args(["--foo", input_val]) + opts = optmanager.parse_args(["--foo", input_val]) assert opts.foo == expected def test_optparse_normalize_choice_type(optmanager): """Test the optparse shim for type="choice".""" optmanager.add_option("--foo", type="choice", choices=("1", "2", "3")) - opts, args = optmanager.parse_args(["--foo", "1"]) + opts = optmanager.parse_args(["--foo", "1"]) assert opts.foo == "1" # fails to parse with pytest.raises(SystemExit): From 81a4110338496714c0bf2bb0e8dd929461d9d492 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 16:24:59 -0800 Subject: [PATCH 026/257] have application return exit code for easier testing --- src/flake8/__main__.py | 5 +++-- src/flake8/main/application.py | 26 +++++++++++--------------- src/flake8/main/cli.py | 4 ++-- tests/integration/test_main.py | 15 +++++++++------ tests/unit/test_application.py | 19 ++++++++----------- 5 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/flake8/__main__.py b/src/flake8/__main__.py index 42bc428..de240dc 100644 --- a/src/flake8/__main__.py +++ b/src/flake8/__main__.py @@ -1,4 +1,5 @@ """Module allowing for ``python -m flake8 ...``.""" -from flake8.main import cli +from flake8.main.cli import main -cli.main() +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index c85172c..61e22a8 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -121,22 +121,13 @@ class Application: rest.extend(("--output-file", args.output_file)) return args, rest - def exit(self) -> None: - """Handle finalization and exiting the program. - - This should be the last thing called on the application instance. It - will check certain options and exit appropriately. - """ + def exit_code(self) -> int: + """Return the program exit code.""" assert self.options is not None - if self.options.count: - print(self.result_count) - if self.options.exit_zero: - raise SystemExit(self.catastrophic_failure) + return int(self.catastrophic_failure) else: - raise SystemExit( - (self.result_count > 0) or self.catastrophic_failure - ) + return int((self.result_count > 0) or self.catastrophic_failure) def find_plugins(self, config_finder: config.ConfigFileFinder) -> None: """Find and load the plugins for this application. @@ -193,8 +184,6 @@ class Application: "future version." ) self.parsed_diff = utils.parse_unified_diff() - if not self.parsed_diff: - self.exit() assert self.check_plugins is not None self.check_plugins.provide_options( @@ -268,6 +257,9 @@ class Application: assert self.file_checker_manager is not None if self.running_against_diff: files = sorted(self.parsed_diff) + if not files: + return + self.file_checker_manager.start(files) try: self.file_checker_manager.run() @@ -388,3 +380,7 @@ class Application: except exceptions.EarlyQuit: self.catastrophic_failure = True print("... stopped while processing files") + + assert self.options is not None + if self.options.count: + print(self.result_count) diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py index ddbc7c0..6c0b1ad 100644 --- a/src/flake8/main/cli.py +++ b/src/flake8/main/cli.py @@ -6,7 +6,7 @@ from typing import Optional from flake8.main import application -def main(argv: Optional[List[str]] = None) -> None: +def main(argv: Optional[List[str]] = None) -> int: """Execute the main bit of the application. This handles the creation of an instance of :class:`Application`, runs it, @@ -20,4 +20,4 @@ def main(argv: Optional[List[str]] = None) -> None: app = application.Application() app.run(argv) - app.exit() + return app.exit_code() diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 6c92c4a..bc78c8b 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -11,9 +11,8 @@ from flake8.main import cli def _call_main(argv, retv=0): - with pytest.raises(SystemExit) as excinfo: - cli.main(argv) - assert excinfo.value.code == retv + exit_code = cli.main(argv) + assert exit_code == retv def test_diff_option(tmpdir, capsys): @@ -221,7 +220,9 @@ def test_tokenization_error_is_a_syntax_error(tmpdir, capsys): def test_bug_report_successful(capsys): """Test that --bug-report does not crash.""" - _call_main(["--bug-report"]) + with pytest.raises(SystemExit) as excinfo: + _call_main(["--bug-report"]) + assert excinfo.value.args[0] == 0 out, err = capsys.readouterr() assert json.loads(out) assert err == "" @@ -316,8 +317,10 @@ t.py:1:9: W292 no newline at end of file def test_obtaining_args_from_sys_argv_when_not_explicity_provided(capsys): """Test that arguments are obtained from 'sys.argv'.""" - with mock.patch("sys.argv", ["flake8", "--help"]): - _call_main(None) + with pytest.raises(SystemExit) as excinfo: + with mock.patch("sys.argv", ["flake8", "--help"]): + _call_main(None) + assert excinfo.value.args[0] == 0 out, err = capsys.readouterr() assert out.startswith("usage: flake8 [options] file file ...\n") diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index b95e383..2d0e849 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -26,13 +26,13 @@ def application(): @pytest.mark.parametrize( "result_count, catastrophic, exit_zero, value", [ - (0, False, False, False), - (0, True, False, True), - (2, False, False, True), - (2, True, False, True), - (0, True, True, True), - (2, False, True, False), - (2, True, True, True), + (0, False, False, 0), + (0, True, False, 1), + (2, False, False, 1), + (2, True, False, 1), + (0, True, True, 1), + (2, False, True, 0), + (2, True, True, 1), ], ) def test_exit_does_raise( @@ -43,10 +43,7 @@ def test_exit_does_raise( application.catastrophic_failure = catastrophic application.options = options(exit_zero=exit_zero) - with pytest.raises(SystemExit) as excinfo: - application.exit() - - assert excinfo.value.args[0] is value + assert application.exit_code() == value def test_warns_on_unknown_formatter_plugin_name(application): From 97c3de41bdb1b03a95c009ca3e4a5286df957b52 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 16:38:39 -0800 Subject: [PATCH 027/257] fix test name after exit -> exit_code refactor --- tests/unit/test_application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 2d0e849..9f4b1e8 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -35,10 +35,10 @@ def application(): (2, True, True, 1), ], ) -def test_exit_does_raise( +def test_application_exit_code( result_count, catastrophic, exit_zero, value, application ): - """Verify Application.exit doesn't raise SystemExit.""" + """Verify Application.exit_code returns the correct value.""" application.result_count = result_count application.catastrophic_failure = catastrophic application.options = options(exit_zero=exit_zero) From 66071563c2b592a77488993e064531b0005f7001 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 19:53:35 -0800 Subject: [PATCH 028/257] split out file discovery and test it --- docs/source/internal/utils.rst | 18 --- setup.cfg | 2 + src/flake8/checker.py | 62 ++-------- src/flake8/discover_files.py | 96 ++++++++++++++++ src/flake8/utils.py | 50 +-------- tests/unit/test_checker_manager.py | 8 +- tests/unit/test_discover_files.py | 174 +++++++++++++++++++++++++++++ tests/unit/test_utils.py | 97 ++-------------- tox.ini | 1 + 9 files changed, 291 insertions(+), 217 deletions(-) create mode 100644 src/flake8/discover_files.py create mode 100644 tests/unit/test_discover_files.py diff --git a/docs/source/internal/utils.rst b/docs/source/internal/utils.rst index 387ec82..34b09f1 100644 --- a/docs/source/internal/utils.rst +++ b/docs/source/internal/utils.rst @@ -60,24 +60,6 @@ Another helpful function that is named only to be explicit given it is a very trivial check, this checks if the user specified ``-`` in their arguments to |Flake8| to indicate we should read from stdin. -.. autofunction:: flake8.utils.filenames_from - -When provided an argument to |Flake8|, we need to be able to traverse -directories in a convenient manner. For example, if someone runs - -.. code:: - - $ flake8 flake8/ - -Then they want us to check all of the files in the directory ``flake8/``. This -function will handle that while also handling the case where they specify a -file like: - -.. code:: - - $ flake8 flake8/__init__.py - - .. autofunction:: flake8.utils.fnmatch The standard library's :func:`fnmatch.fnmatch` is excellent at deciding if a diff --git a/setup.cfg b/setup.cfg index fca70c6..1477769 100644 --- a/setup.cfg +++ b/setup.cfg @@ -111,6 +111,8 @@ warn_unused_ignores = true disallow_untyped_defs = true [mypy-flake8.defaults] disallow_untyped_defs = true +[mypy-flake8.discover_files] +disallow_untyped_defs = true [mypy-flake8.exceptions] disallow_untyped_defs = true [mypy-flake8.formatting.*] diff --git a/src/flake8/checker.py b/src/flake8/checker.py index cbff46f..dba5d08 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -15,6 +15,7 @@ from flake8 import defaults from flake8 import exceptions from flake8 import processor from flake8 import utils +from flake8.discover_files import expand_paths Results = List[Tuple[str, int, int, str, Optional[str]]] @@ -155,70 +156,21 @@ class Manager: ) return reported_results_count - def is_path_excluded(self, path: str) -> bool: - """Check if a path is excluded. - - :param str path: - Path to check against the exclude patterns. - :returns: - True if there are exclude patterns and the path matches, - otherwise False. - :rtype: - bool - """ - if path == "-": - if self.options.stdin_display_name == "stdin": - return False - path = self.options.stdin_display_name - - return utils.matches_filename( - path, - patterns=self.exclude, - log_message='"%(path)s" has %(whether)sbeen excluded', - logger=LOG, - ) - def make_checkers(self, paths: Optional[List[str]] = None) -> None: """Create checkers for each file.""" if paths is None: paths = self.arguments - if not paths: - paths = ["."] - - filename_patterns = self.options.filename - running_from_diff = self.options.diff - - # NOTE(sigmavirus24): Yes this is a little unsightly, but it's our - # best solution right now. - def should_create_file_checker(filename, argument): - """Determine if we should create a file checker.""" - matches_filename_patterns = utils.fnmatch( - filename, filename_patterns - ) - is_stdin = filename == "-" - # NOTE(sigmavirus24): If a user explicitly specifies something, - # e.g, ``flake8 bin/script`` then we should run Flake8 against - # that. Since should_create_file_checker looks to see if the - # filename patterns match the filename, we want to skip that in - # the event that the argument and the filename are identical. - # If it was specified explicitly, the user intended for it to be - # checked. - explicitly_provided = not running_from_diff and ( - argument == filename - ) - return ( - explicitly_provided or matches_filename_patterns - ) or is_stdin - checks = self.checks.to_dictionary() self._all_checkers = [ FileChecker(filename, checks, self.options) - for argument in paths - for filename in utils.filenames_from( - argument, self.is_path_excluded + for filename in expand_paths( + paths=paths, + stdin_display_name=self.options.stdin_display_name, + filename_patterns=self.options.filename, + exclude=self.exclude, + is_running_from_diff=self.options.diff, ) - if should_create_file_checker(filename, argument) ] self.checkers = [c for c in self._all_checkers if c.should_process] LOG.info("Checking %d files", len(self.checkers)) diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py new file mode 100644 index 0000000..f51b9d0 --- /dev/null +++ b/src/flake8/discover_files.py @@ -0,0 +1,96 @@ +"""Functions related to discovering paths.""" +import logging +import os.path +from typing import Callable +from typing import Generator +from typing import Sequence + +from flake8 import utils + +LOG = logging.getLogger(__name__) + + +def _filenames_from( + arg: str, + *, + predicate: Callable[[str], bool], +) -> Generator[str, None, None]: + """Generate filenames from an argument. + + :param str arg: + Parameter from the command-line. + :param callable predicate: + Predicate to use to filter out filenames. If the predicate + returns ``True`` we will exclude the filename, otherwise we + will yield it. By default, we include every filename + generated. + :returns: + Generator of paths + """ + if predicate(arg): + return + + if os.path.isdir(arg): + for root, sub_directories, files in os.walk(arg): + # NOTE(sigmavirus24): os.walk() will skip a directory if you + # remove it from the list of sub-directories. + for directory in tuple(sub_directories): + joined = os.path.join(root, directory) + if predicate(joined): + sub_directories.remove(directory) + + for filename in files: + joined = os.path.join(root, filename) + if not predicate(joined): + yield joined + else: + yield arg + + +def expand_paths( + *, + paths: Sequence[str], + stdin_display_name: str, + filename_patterns: Sequence[str], + exclude: Sequence[str], + is_running_from_diff: bool, +) -> Generator[str, None, None]: + """Expand out ``paths`` from commandline to the lintable files.""" + if not paths: + paths = ["."] + + def is_excluded(arg: str) -> bool: + if arg == "-": + # if the stdin_display_name is the default, always include it + if stdin_display_name == "stdin": + return False + arg = stdin_display_name + + return utils.matches_filename( + arg, + patterns=exclude, + log_message='"%(path)s" has %(whether)sbeen excluded', + logger=LOG, + ) + + def is_included(arg: str, fname: str) -> bool: + # while running from a diff, the arguments aren't _explicitly_ + # listed so we still filter them + if is_running_from_diff: + return utils.fnmatch(fname, filename_patterns) + else: + return ( + # always lint `-` + fname == "-" + # always lint explicitly passed (even if not matching filter) + or arg == fname + # otherwise, check the file against filtered patterns + or utils.fnmatch(fname, filename_patterns) + ) + + return ( + filename + for path in paths + for filename in _filenames_from(path, predicate=is_excluded) + if is_included(path, filename) + ) diff --git a/src/flake8/utils.py b/src/flake8/utils.py index 4e68104..9eb2497 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -11,9 +11,7 @@ import re import sys import textwrap import tokenize -from typing import Callable from typing import Dict -from typing import Generator from typing import List from typing import Optional from typing import Pattern @@ -294,52 +292,6 @@ def is_using_stdin(paths: List[str]) -> bool: return "-" in paths -def _default_predicate(*args: str) -> bool: - return False - - -def filenames_from( - arg: str, predicate: Optional[Callable[[str], bool]] = None -) -> Generator[str, None, None]: - """Generate filenames from an argument. - - :param str arg: - Parameter from the command-line. - :param callable predicate: - Predicate to use to filter out filenames. If the predicate - returns ``True`` we will exclude the filename, otherwise we - will yield it. By default, we include every filename - generated. - :returns: - Generator of paths - """ - if predicate is None: - predicate = _default_predicate - - if predicate(arg): - return - - if os.path.isdir(arg): - for root, sub_directories, files in os.walk(arg): - if predicate(root): - sub_directories[:] = [] - continue - - # NOTE(sigmavirus24): os.walk() will skip a directory if you - # remove it from the list of sub-directories. - for directory in sub_directories: - joined = os.path.join(root, directory) - if predicate(joined): - sub_directories.remove(directory) - - for filename in files: - joined = os.path.join(root, filename) - if not predicate(joined): - yield joined - else: - yield arg - - def fnmatch(filename: str, patterns: Sequence[str]) -> bool: """Wrap :func:`fnmatch.fnmatch` to add some functionality. @@ -351,7 +303,7 @@ def fnmatch(filename: str, patterns: Sequence[str]) -> bool: The default value if patterns is empty :returns: True if a pattern matches the filename, False if it doesn't. - ``default`` if patterns is empty. + ``True`` if patterns is empty. """ if not patterns: return True diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 8e45675..6594453 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -85,11 +85,9 @@ def test_make_checkers(_): } manager = checker.Manager(style_guide, files, checkplugins) - with mock.patch("flake8.utils.filenames_from") as filenames_from: - filenames_from.side_effect = [["file1"], ["file2"]] - with mock.patch("flake8.utils.fnmatch", return_value=True): - with mock.patch("flake8.processor.FileProcessor"): - manager.make_checkers() + with mock.patch("flake8.utils.fnmatch", return_value=True): + with mock.patch("flake8.processor.FileProcessor"): + manager.make_checkers(["file1", "file2"]) assert manager._all_checkers for file_checker in manager._all_checkers: diff --git a/tests/unit/test_discover_files.py b/tests/unit/test_discover_files.py new file mode 100644 index 0000000..f2dfa13 --- /dev/null +++ b/tests/unit/test_discover_files.py @@ -0,0 +1,174 @@ +import os.path + +import pytest + +from flake8 import utils +from flake8.discover_files import _filenames_from +from flake8.discover_files import expand_paths + + +@pytest.fixture +def files_dir(tmpdir): + """Create test dir for testing filenames_from.""" + with tmpdir.as_cwd(): + tmpdir.join("a/b/c.py").ensure() + tmpdir.join("a/b/d.py").ensure() + tmpdir.join("a/b/e/f.py").ensure() + yield tmpdir + + +def _noop(path): + return False + + +def _normpath(s): + return s.replace("/", os.sep) + + +def _normpaths(pths): + return {_normpath(pth) for pth in pths} + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_directory(): + """Verify that filenames_from walks a directory.""" + filenames = set(_filenames_from(_normpath("a/b/"), predicate=_noop)) + # should include all files + expected = _normpaths(("a/b/c.py", "a/b/d.py", "a/b/e/f.py")) + assert filenames == expected + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_directory_with_a_predicate(): + """Verify that predicates filter filenames_from.""" + filenames = set( + _filenames_from( + arg=_normpath("a/b/"), + predicate=lambda path: path.endswith(_normpath("b/c.py")), + ) + ) + # should not include c.py + expected = _normpaths(("a/b/d.py", "a/b/e/f.py")) + assert filenames == expected + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_directory_with_a_predicate_from_the_current_dir(): + """Verify that predicates filter filenames_from.""" + filenames = set( + _filenames_from( + arg=_normpath("./a/b"), + predicate=lambda path: path == "c.py", + ) + ) + # none should have matched the predicate so all returned + expected = _normpaths(("./a/b/c.py", "./a/b/d.py", "./a/b/e/f.py")) + assert filenames == expected + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_single_file(): + """Verify that we simply yield that filename.""" + filenames = set(_filenames_from(_normpath("a/b/c.py"), predicate=_noop)) + assert filenames == {_normpath("a/b/c.py")} + + +def test_filenames_from_a_single_file_does_not_exist(): + """Verify that a passed filename which does not exist is returned back.""" + filenames = set(_filenames_from(_normpath("d/n/e.py"), predicate=_noop)) + assert filenames == {_normpath("d/n/e.py")} + + +def test_filenames_from_exclude_doesnt_exclude_directory_names(tmpdir): + """Verify that we don't greedily exclude subdirs.""" + tmpdir.join("1/dont_return_me.py").ensure() + tmpdir.join("2/1/return_me.py").ensure() + exclude = [tmpdir.join("1").strpath] + + def predicate(pth): + return utils.fnmatch(os.path.abspath(pth), exclude) + + with tmpdir.as_cwd(): + filenames = list(_filenames_from(".", predicate=predicate)) + assert filenames == [os.path.join(".", "2", "1", "return_me.py")] + + +def test_filenames_from_predicate_applies_to_initial_arg(tmp_path): + """Test that the predicate is also applied to the passed argument.""" + fname = str(tmp_path.joinpath("f.py")) + ret = tuple(_filenames_from(fname, predicate=lambda _: True)) + assert ret == () + + +def test_filenames_from_predicate_applies_to_dirname(tmp_path): + """Test that the predicate can filter whole directories.""" + a_dir = tmp_path.joinpath("a") + a_dir.mkdir() + a_dir.joinpath("b.py").touch() + + b_py = tmp_path.joinpath("b.py") + b_py.touch() + + def predicate(p): + # filter out the /a directory + return p.endswith("a") + + ret = tuple(_filenames_from(str(tmp_path), predicate=predicate)) + assert ret == (str(b_py),) + + +def _expand_paths( + *, + paths=(".",), + stdin_display_name="stdin", + filename_patterns=("*.py",), + exclude=(), + is_running_from_diff=False, +): + return set( + expand_paths( + paths=paths, + stdin_display_name=stdin_display_name, + filename_patterns=filename_patterns, + exclude=exclude, + is_running_from_diff=is_running_from_diff, + ) + ) + + +@pytest.mark.usefixtures("files_dir") +def test_expand_paths_honors_exclude(): + expected = _normpaths(("./a/b/c.py", "./a/b/e/f.py")) + assert _expand_paths(exclude=["d.py"]) == expected + + +@pytest.mark.usefixtures("files_dir") +def test_expand_paths_defaults_to_dot(): + expected = _normpaths(("./a/b/c.py", "./a/b/d.py", "./a/b/e/f.py")) + assert _expand_paths(paths=()) == expected + + +def test_default_stdin_name_is_not_filtered(): + assert _expand_paths(paths=("-",)) == {"-"} + + +def test_alternate_stdin_name_is_filtered(): + ret = _expand_paths( + paths=("-",), + stdin_display_name="wat", + exclude=("wat",), + ) + assert ret == set() + + +def test_filename_included_even_if_not_matching_include(tmp_path): + some_file = str(tmp_path.joinpath("some/file")) + assert _expand_paths(paths=(some_file,)) == {some_file} + + +def test_diff_filenames_filtered_by_patterns(tmp_path): + f1 = str(tmp_path.joinpath("f1")) + f2 = str(tmp_path.joinpath("f2.py")) + + ret = _expand_paths(paths=(f1, f2), is_running_from_diff=True) + assert ret == {f2} diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 947b67f..eb76572 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -160,6 +160,13 @@ def test_normalize_paths(value, expected): assert utils.normalize_paths(value) == expected +def test_matches_filename_for_excluding_dotfiles(): + """Verify that `.` and `..` are not matched by `.*`.""" + logger = logging.Logger(__name__) + assert not utils.matches_filename(".", (".*",), "", logger) + assert not utils.matches_filename("..", (".*",), "", logger) + + @pytest.mark.parametrize( "filename,patterns,expected", [ @@ -174,89 +181,6 @@ def test_fnmatch(filename, patterns, expected): assert utils.fnmatch(filename, patterns) is expected -@pytest.fixture -def files_dir(tmpdir): - """Create test dir for testing filenames_from.""" - with tmpdir.as_cwd(): - tmpdir.join("a/b/c.py").ensure() - tmpdir.join("a/b/d.py").ensure() - tmpdir.join("a/b/e/f.py").ensure() - yield tmpdir - - -def _normpath(s): - return s.replace("/", os.sep) - - -def _normpaths(pths): - return {_normpath(pth) for pth in pths} - - -@pytest.mark.usefixtures("files_dir") -def test_filenames_from_a_directory(): - """Verify that filenames_from walks a directory.""" - filenames = set(utils.filenames_from(_normpath("a/b/"))) - # should include all files - expected = _normpaths(("a/b/c.py", "a/b/d.py", "a/b/e/f.py")) - assert filenames == expected - - -@pytest.mark.usefixtures("files_dir") -def test_filenames_from_a_directory_with_a_predicate(): - """Verify that predicates filter filenames_from.""" - filenames = set( - utils.filenames_from( - arg=_normpath("a/b/"), - predicate=lambda path: path.endswith(_normpath("b/c.py")), - ) - ) - # should not include c.py - expected = _normpaths(("a/b/d.py", "a/b/e/f.py")) - assert filenames == expected - - -@pytest.mark.usefixtures("files_dir") -def test_filenames_from_a_directory_with_a_predicate_from_the_current_dir(): - """Verify that predicates filter filenames_from.""" - filenames = set( - utils.filenames_from( - arg=_normpath("./a/b"), - predicate=lambda path: path == "c.py", - ) - ) - # none should have matched the predicate so all returned - expected = _normpaths(("./a/b/c.py", "./a/b/d.py", "./a/b/e/f.py")) - assert filenames == expected - - -@pytest.mark.usefixtures("files_dir") -def test_filenames_from_a_single_file(): - """Verify that we simply yield that filename.""" - filenames = set(utils.filenames_from(_normpath("a/b/c.py"))) - assert filenames == {_normpath("a/b/c.py")} - - -def test_filenames_from_a_single_file_does_not_exist(): - """Verify that a passed filename which does not exist is returned back.""" - filenames = set(utils.filenames_from(_normpath("d/n/e.py"))) - assert filenames == {_normpath("d/n/e.py")} - - -def test_filenames_from_exclude_doesnt_exclude_directory_names(tmpdir): - """Verify that we don't greedily exclude subdirs.""" - tmpdir.join("1").ensure_dir().join("dont_return_me.py").ensure() - tmpdir.join("2").join("1").ensure_dir().join("return_me.py").ensure() - exclude = [tmpdir.join("1").strpath] - - # This acts similar to src.flake8.checker.is_path_excluded - def predicate(pth): - return utils.fnmatch(os.path.abspath(pth), exclude) - - with tmpdir.as_cwd(): - filenames = list(utils.filenames_from(".", predicate)) - assert filenames == [os.path.join(".", "2", "1", "return_me.py")] - - def test_parameters_for_class_plugin(): """Verify that we can retrieve the parameters for a class plugin.""" @@ -323,13 +247,6 @@ def test_parse_unified_diff(diff, parsed_diff): assert utils.parse_unified_diff(diff) == parsed_diff -def test_matches_filename_for_excluding_dotfiles(): - """Verify that `.` and `..` are not matched by `.*`.""" - logger = logging.Logger(__name__) - assert not utils.matches_filename(".", (".*",), "", logger) - assert not utils.matches_filename("..", (".*",), "", logger) - - def test_stdin_get_value_crlf(): """Ensure that stdin is normalized from crlf to lf.""" stdin = io.TextIOWrapper(io.BytesIO(b"1\r\n2\r\n"), "UTF-8") diff --git a/tox.ini b/tox.ini index 4f355d8..6b864e2 100644 --- a/tox.ini +++ b/tox.ini @@ -122,4 +122,5 @@ commands = extend-ignore = E203 per-file-ignores = src/flake8/formatting/_windows_color.py: N806 + tests/*: D max-complexity = 10 From bbbe0d80486d2bd07691271ac34a247be3da7cf7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 20:36:32 -0800 Subject: [PATCH 029/257] improve integration tests --- src/flake8/main/application.py | 9 +++--- src/flake8/main/cli.py | 4 +-- tests/integration/test_main.py | 53 ++++++++++++++----------------- tests/integration/test_plugins.py | 30 +++++++++++++++++ 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 61e22a8..70165c0 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -6,6 +6,7 @@ import time from typing import Dict from typing import List from typing import Optional +from typing import Sequence from typing import Set from typing import Tuple from typing import Type @@ -96,7 +97,7 @@ class Application: self.parsed_diff: Dict[str, Set[int]] = {} def parse_preliminary_options( - self, argv: List[str] + self, argv: Sequence[str] ) -> Tuple[argparse.Namespace, List[str]]: """Get preliminary options from the CLI, pre-plugin-loading. @@ -318,7 +319,7 @@ class Application: assert self.guide is not None self.formatter.show_statistics(self.guide.stats) - def initialize(self, argv: List[str]) -> None: + def initialize(self, argv: Sequence[str]) -> None: """Initialize the application to be run. This finds the plugins, registers their options, and parses the @@ -353,12 +354,12 @@ class Application: self.report_benchmarks() self.formatter.stop() - def _run(self, argv: List[str]) -> None: + def _run(self, argv: Sequence[str]) -> None: self.initialize(argv) self.run_checks() self.report() - def run(self, argv: List[str]) -> None: + def run(self, argv: Sequence[str]) -> None: """Run our application. This method will also handle KeyboardInterrupt exceptions for the diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py index 6c0b1ad..8d7ea9a 100644 --- a/src/flake8/main/cli.py +++ b/src/flake8/main/cli.py @@ -1,12 +1,12 @@ """Command-line implementation of flake8.""" import sys -from typing import List from typing import Optional +from typing import Sequence from flake8.main import application -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: Optional[Sequence[str]] = None) -> int: """Execute the main bit of the application. This handles the creation of an instance of :class:`Application`, runs it, diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index bc78c8b..27acab5 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -10,11 +10,6 @@ from flake8 import utils from flake8.main import cli -def _call_main(argv, retv=0): - exit_code = cli.main(argv) - assert exit_code == retv - - def test_diff_option(tmpdir, capsys): """Ensure that `flake8 --diff` works.""" t_py_contents = """\ @@ -44,7 +39,7 @@ index d64ac39..7d943de 100644 with mock.patch.object(utils, "stdin_get_value", return_value=diff): with tmpdir.as_cwd(): tmpdir.join("t.py").write(t_py_contents) - _call_main(["--diff"], retv=1) + assert cli.main(["--diff"]) == 1 out, err = capsys.readouterr() assert out == "t.py:8:1: F821 undefined name 'y'\n" @@ -63,12 +58,12 @@ t.py:3:2: E225 missing whitespace around operator tmpdir.join("t.py").write(src) with mock.patch.object(utils, "stdin_get_value", return_value=src): - _call_main(["-", "--stdin-display-name=t.py"], retv=1) + assert cli.main(["-", "--stdin-display-name=t.py"]) == 1 out, err = capsys.readouterr() assert out == expected_out assert err == "" - _call_main(["t.py"], retv=1) + assert cli.main(["t.py"]) == 1 out, err = capsys.readouterr() assert out == expected_out assert err == "" @@ -90,14 +85,14 @@ if True: with tmpdir.as_cwd(): tmpdir.join("t.py").write(t_py_contents) - _call_main(["t.py"]) + assert cli.main(["t.py"]) == 0 def test_statistics_option(tmpdir, capsys): """Ensure that `flake8 --statistics` works.""" with tmpdir.as_cwd(): tmpdir.join("t.py").write("import os\nimport sys\n") - _call_main(["--statistics", "t.py"], retv=1) + assert cli.main(["--statistics", "t.py"]) == 1 expected = """\ t.py:1:1: F401 'os' imported but unused @@ -114,7 +109,7 @@ def test_show_source_option(tmpdir, capsys): with tmpdir.as_cwd(): tmpdir.join("tox.ini").write("[flake8]\nshow_source = true\n") tmpdir.join("t.py").write("import os\n") - _call_main(["t.py"], retv=1) + assert cli.main(["t.py"]) == 1 expected = """\ t.py:1:1: F401 'os' imported but unused @@ -126,7 +121,7 @@ import os assert err == "" with tmpdir.as_cwd(): - _call_main(["t.py", "--no-show-source"], retv=1) + assert cli.main(["t.py", "--no-show-source"]) == 1 expected = """\ t.py:1:1: F401 'os' imported but unused @@ -142,7 +137,7 @@ def test_extend_exclude(tmpdir, capsys): tmpdir.mkdir(d).join("t.py").write("import os\nimport sys\n") with tmpdir.as_cwd(): - _call_main(["--extend-exclude=vendor,legacy/"], retv=1) + assert cli.main(["--extend-exclude=vendor,legacy/"]) == 1 out, err = capsys.readouterr() expected_out = """\ @@ -173,7 +168,7 @@ Configured `per-file-ignores` setting: with tmpdir.as_cwd(): tmpdir.join("setup.cfg").write(setup_cfg) - _call_main(["."], retv=1) + assert cli.main(["."]) == 1 out, err = capsys.readouterr() assert out == expected @@ -184,7 +179,7 @@ def test_tokenization_error_but_not_syntax_error(tmpdir, capsys): with tmpdir.as_cwd(): # this is a crash in the tokenizer, but not in the ast tmpdir.join("t.py").write("b'foo' \\\n") - _call_main(["t.py"], retv=1) + assert cli.main(["t.py"]) == 1 if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) expected = "t.py:2:1: E999 SyntaxError: end of file (EOF) in multi-line statement\n" # noqa: E501 @@ -204,7 +199,7 @@ def test_tokenization_error_is_a_syntax_error(tmpdir, capsys): """Test when tokenize raises a SyntaxError.""" with tmpdir.as_cwd(): tmpdir.join("t.py").write("if True:\n pass\n pass\n") - _call_main(["t.py"], retv=1) + assert cli.main(["t.py"]) == 1 if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) expected = "t.py:3:2: E999 IndentationError: unindent does not match any outer indentation level\n" # noqa: E501 @@ -221,7 +216,7 @@ def test_tokenization_error_is_a_syntax_error(tmpdir, capsys): def test_bug_report_successful(capsys): """Test that --bug-report does not crash.""" with pytest.raises(SystemExit) as excinfo: - _call_main(["--bug-report"]) + cli.main(["--bug-report"]) assert excinfo.value.args[0] == 0 out, err = capsys.readouterr() assert json.loads(out) @@ -233,7 +228,7 @@ def test_benchmark_successful(tmp_path, capsys): fname = tmp_path.joinpath("t.py") fname.write_text("print('hello world')\n") - _call_main(["--benchmark", str(fname)]) + assert cli.main(["--benchmark", str(fname)]) == 0 out, err = capsys.readouterr() parts = [line.split(maxsplit=1) for line in out.splitlines()] @@ -255,7 +250,7 @@ def test_specific_noqa_does_not_clobber_pycodestyle_noqa(tmpdir, capsys): """See https://github.com/pycqa/flake8/issues/1104.""" with tmpdir.as_cwd(): tmpdir.join("t.py").write("test = ('ABC' == None) # noqa: E501\n") - _call_main(["t.py"], retv=1) + assert cli.main(["t.py"]) == 1 expected = """\ t.py:1:15: E711 comparison to None should be 'if cond is None:' @@ -277,7 +272,7 @@ x = """ with tmpdir.as_cwd(): tmpdir.join("t.py").write(t_py_src) - _call_main(["t.py"], retv=0) + assert cli.main(["t.py"]) == 0 out, err = capsys.readouterr() assert out == err == "" @@ -289,7 +284,7 @@ def test_physical_line_file_not_ending_in_newline(tmpdir, capsys): with tmpdir.as_cwd(): tmpdir.join("t.py").write(t_py_src) - _call_main(["t.py"], retv=1) + assert cli.main(["t.py"]) == 1 expected = """\ t.py:2:1: W191 indentation contains tabs @@ -305,7 +300,7 @@ def test_physical_line_file_not_ending_in_newline_trailing_ws(tmpdir, capsys): with tmpdir.as_cwd(): tmpdir.join("t.py").write(t_py_src) - _call_main(["t.py"], retv=1) + assert cli.main(["t.py"]) == 1 expected = """\ t.py:1:6: W291 trailing whitespace @@ -317,9 +312,9 @@ t.py:1:9: W292 no newline at end of file def test_obtaining_args_from_sys_argv_when_not_explicity_provided(capsys): """Test that arguments are obtained from 'sys.argv'.""" - with pytest.raises(SystemExit) as excinfo: - with mock.patch("sys.argv", ["flake8", "--help"]): - _call_main(None) + with mock.patch("sys.argv", ["flake8", "--help"]): + with pytest.raises(SystemExit) as excinfo: + cli.main() assert excinfo.value.args[0] == 0 out, err = capsys.readouterr() @@ -340,7 +335,7 @@ ignore = F401 py_file = tmp_path / "t.py" py_file.write_text("import os\n") - _call_main(["--config", str(config), str(py_file)]) + assert cli.main(["--config", str(config), str(py_file)]) == 0 def test_cli_isolated_overrides_config_option(tmp_path): @@ -356,13 +351,13 @@ ignore = F401 py_file = tmp_path / "t.py" py_file.write_text("import os\n") - _call_main(["--isolated", "--config", str(config), str(py_file)], retv=1) + assert cli.main(["--isolated", "--config", str(config), str(py_file)]) == 1 def test_file_not_found(tmpdir, capsys): """Ensure that a not-found file / directory is an error.""" with tmpdir.as_cwd(): - _call_main(["i-do-not-exist"], retv=1) + assert cli.main(["i-do-not-exist"]) == 1 out, err = capsys.readouterr() assert out.startswith("i-do-not-exist:0:1: E902") assert err == "" @@ -373,7 +368,7 @@ def test_output_file(tmpdir, capsys): tmpdir.join("t.py").write("import os\n") with tmpdir.as_cwd(): - _call_main(["t.py", "--output-file=a/b/f"], retv=1) + assert cli.main(["t.py", "--output-file=a/b/f"]) == 1 out, err = capsys.readouterr() assert out == err == "" diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 7fff9df..dd7b9b7 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -1,5 +1,6 @@ """Integration tests for plugin loading.""" from flake8.main import application +from flake8.main.cli import main LOCAL_PLUGIN_CONFIG = "tests/fixtures/config_files/local-plugin.ini" LOCAL_PLUGIN_PATH_CONFIG = "tests/fixtures/config_files/local-plugin-path.ini" @@ -65,3 +66,32 @@ def test_enable_local_plugin_at_non_installed_path(): assert app.check_plugins is not None assert app.check_plugins["XE"].plugin.name == "ExtensionTestPlugin2" + + +class AlwaysErrors: + name = "AlwaysError" + version = "1" + + def __init__(self, tree): + pass + + def run(self): + yield 1, 0, "ABC123 error", type(self) + + +def test_plugin_gets_enabled_by_default(tmp_path, capsys): + cfg_s = f"""\ +[flake8:local-plugins] +extension = + ABC = {AlwaysErrors.__module__}:{AlwaysErrors.__name__} +""" + cfg = tmp_path.joinpath("tox.ini") + cfg.write_text(cfg_s) + + t_py = tmp_path.joinpath("t.py") + t_py.touch() + + assert main((str(t_py), "--config", str(cfg))) == 1 + out, err = capsys.readouterr() + assert out == f"{t_py}:1:1: ABC123 error\n" + assert err == "" From 3b7dbd66973238e5bb0f3bbd5d257ae91ea27489 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 20:59:28 -0800 Subject: [PATCH 030/257] replace py2-kwonly shim with true kwonly args --- src/flake8/main/debug.py | 4 ++-- src/flake8/options/manager.py | 25 +++++++++++++++---------- tests/unit/test_statistics.py | 14 ++++++-------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index 9f087c9..9b3e42b 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -9,13 +9,13 @@ from typing import List class DebugAction(argparse.Action): """argparse action to print debug information.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, option_manager, **kwargs): """Initialize the action. This takes an extra `option_manager` keyword argument which will be used to delay response. """ - self._option_manager = kwargs.pop("option_manager") + self._option_manager = option_manager super().__init__(*args, **kwargs) def __call__(self, parser, namespace, values, option_string=None): diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index af6f31c..f9765e1 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -43,10 +43,17 @@ _optparse_callable_map: Dict[str, Union[Type[Any], _ARG]] = { class _CallbackAction(argparse.Action): """Shim for optparse-style callback actions.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - self._callback = kwargs.pop("callback") - self._callback_args = kwargs.pop("callback_args", ()) - self._callback_kwargs = kwargs.pop("callback_kwargs", {}) + def __init__( + self, + *args: Any, + callback: Callable[..., Any], + callback_args: Sequence[Any] = (), + callback_kwargs: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> None: + self._callback = callback + self._callback_args = callback_args + self._callback_kwargs = callback_kwargs or {} super().__init__(*args, **kwargs) def __call__( @@ -71,13 +78,11 @@ class _CallbackAction(argparse.Action): def _flake8_normalize( - value: str, *args: str, **kwargs: bool + value: str, + *args: str, + comma_separated_list: bool = False, + normalize_paths: bool = False, ) -> Union[str, List[str]]: - comma_separated_list = kwargs.pop("comma_separated_list", False) - normalize_paths = kwargs.pop("normalize_paths", False) - if kwargs: - raise TypeError(f"Unexpected keyword args: {kwargs}") - ret: Union[str, List[str]] = value if comma_separated_list and isinstance(ret, str): ret = utils.parse_comma_separated_list(value) diff --git a/tests/unit/test_statistics.py b/tests/unit/test_statistics.py index 66565c3..9937916 100644 --- a/tests/unit/test_statistics.py +++ b/tests/unit/test_statistics.py @@ -11,14 +11,12 @@ DEFAULT_TEXT = "Default text" def make_error(**kwargs): """Create errors with a bunch of default values.""" - return style_guide.Violation( - code=kwargs.pop("code", DEFAULT_ERROR_CODE), - filename=kwargs.pop("filename", DEFAULT_FILENAME), - line_number=kwargs.pop("line_number", 1), - column_number=kwargs.pop("column_number", 1), - text=kwargs.pop("text", DEFAULT_TEXT), - physical_line=None, - ) + kwargs.setdefault("code", DEFAULT_ERROR_CODE) + kwargs.setdefault("filename", DEFAULT_FILENAME) + kwargs.setdefault("line_number", 1) + kwargs.setdefault("column_number", 1) + kwargs.setdefault("text", DEFAULT_TEXT) + return style_guide.Violation(**kwargs, physical_line=None) def test_key_creation(): From 411ff24392a9e19fd5f65aa38d95d7e93c863fa9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Nov 2021 21:35:40 -0800 Subject: [PATCH 031/257] move from allowlist to blocklist for mypy --- setup.cfg | 38 ++++++++------------ src/flake8/checker.py | 20 +++++------ src/flake8/main/application.py | 6 ++-- src/flake8/main/options.py | 5 +-- src/flake8/plugins/pyflakes.py | 18 +++++++--- tests/fixtures/config_files/local-plugin.ini | 4 +-- tests/integration/__init__.py | 0 tests/integration/subdir/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/test_get_local_plugins.py | 8 +++-- tests/unit/test_pyflakes_codes.py | 2 +- 11 files changed, 53 insertions(+), 48 deletions(-) create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/subdir/__init__.py create mode 100644 tests/unit/__init__.py diff --git a/setup.cfg b/setup.cfg index 1477769..372bbb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -102,33 +102,23 @@ universal = 1 check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true -# TODO: disallow_untyped_defs = true +disallow_untyped_defs = true no_implicit_optional = true warn_unused_ignores = true -# TODO: until we opt in all the modules -[mypy-flake8.__init__] -disallow_untyped_defs = true -[mypy-flake8.defaults] -disallow_untyped_defs = true -[mypy-flake8.discover_files] -disallow_untyped_defs = true -[mypy-flake8.exceptions] -disallow_untyped_defs = true -[mypy-flake8.formatting.*] -disallow_untyped_defs = true -[mypy-flake8.options.manager] -disallow_untyped_defs = true -[mypy-flake8.main.cli] -disallow_untyped_defs = true -[mypy-flake8.processor] -disallow_untyped_defs = true -[mypy-flake8.statistics] -disallow_untyped_defs = true -[mypy-flake8.style_guide] -disallow_untyped_defs = true -[mypy-flake8.utils] -disallow_untyped_defs = true +# TODO: fix these +[mypy-flake8.api.legacy] +disallow_untyped_defs = false +[mypy-flake8.checker] +disallow_untyped_defs = false +[mypy-flake8.main.application] +disallow_untyped_defs = false +[mypy-flake8.main.debug] +disallow_untyped_defs = false +[mypy-flake8.options.config] +disallow_untyped_defs = false +[mypy-flake8.plugins.manager] +disallow_untyped_defs = false [mypy-tests.*] disallow_untyped_defs = false diff --git a/src/flake8/checker.py b/src/flake8/checker.py index dba5d08..059d05c 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -87,7 +87,7 @@ class Manager: itertools.chain(self.options.exclude, self.options.extend_exclude) ) - def _process_statistics(self): + def _process_statistics(self) -> None: for checker in self.checkers: for statistic in defaults.STATISTIC_NAMES: self.statistics[statistic] += checker.statistics[statistic] @@ -142,7 +142,7 @@ class Manager: # it to an integer return jobs.n_jobs - def _handle_results(self, filename, results): + def _handle_results(self, filename: str, results: Results) -> int: style_guide = self.style_guide reported_results_count = 0 for (error_code, line_number, column, text, physical_line) in results: @@ -258,7 +258,7 @@ class Manager: LOG.warning("Flake8 was interrupted by the user") raise exceptions.EarlyQuit("Early quit while running checks") - def start(self, paths=None): + def start(self, paths: Optional[List[str]] = None) -> None: """Start checking files. :param list paths: @@ -268,7 +268,7 @@ class Manager: LOG.info("Making checkers") self.make_checkers(paths) - def stop(self): + def stop(self) -> None: """Stop checking files.""" self._process_statistics() @@ -450,7 +450,7 @@ class FileChecker: text=text, ) - def run_logical_checks(self): + def run_logical_checks(self) -> None: """Run all checks expecting a logical line.""" assert self.processor is not None comments, logical_line, mapping = self.processor.build_logical_line() @@ -476,7 +476,7 @@ class FileChecker: self.processor.next_logical_line() - def run_physical_checks(self, physical_line): + def run_physical_checks(self, physical_line: str) -> None: """Run all checks for a given physical line. A single physical check may return multiple errors. @@ -507,7 +507,7 @@ class FileChecker: text=text, ) - def process_tokens(self): + def process_tokens(self) -> None: """Process tokens and trigger checks. Instead of using this directly, you should use @@ -551,7 +551,7 @@ class FileChecker: self.statistics["logical lines"] = logical_lines return self.filename, self.results, self.statistics - def handle_newline(self, token_type): + def handle_newline(self, token_type: int) -> None: """Handle the logic when encountering a newline token.""" assert self.processor is not None if token_type == tokenize.NEWLINE: @@ -616,7 +616,7 @@ def _try_initialize_processpool( return None -def calculate_pool_chunksize(num_checkers, num_jobs): +def calculate_pool_chunksize(num_checkers: int, num_jobs: int) -> int: """Determine the chunksize for the multiprocessing Pool. - For chunksize, see: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap # noqa @@ -628,7 +628,7 @@ def calculate_pool_chunksize(num_checkers, num_jobs): return max(num_checkers // (num_jobs * 2), 1) -def _run_checks(checker): +def _run_checks(checker: FileChecker) -> Tuple[str, Results, Dict[str, int]]: return checker.run_checks() diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 70165c0..2ed2f68 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -272,7 +272,7 @@ class Application: self.file_checker_manager.stop() self.end_time = time.time() - def report_benchmarks(self): + def report_benchmarks(self) -> None: """Aggregate, calculate, and report benchmarks for this run.""" assert self.options is not None if not self.options.benchmark: @@ -309,7 +309,7 @@ class Application: self.result_count, ) - def report_statistics(self): + def report_statistics(self) -> None: """Aggregate and report statistics from this run.""" assert self.options is not None if not self.options.statistics: @@ -345,7 +345,7 @@ class Application: self.make_guide() self.make_file_checker_manager() - def report(self): + def report(self) -> None: """Report errors, statistics, and benchmarks.""" assert self.formatter is not None self.formatter.start() diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 74440db..babd44b 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -4,6 +4,7 @@ import functools from flake8 import defaults from flake8.main import debug +from flake8.options.manager import OptionManager def register_preliminary_options(parser: argparse.ArgumentParser) -> None: @@ -84,12 +85,12 @@ class JobsArgument: """Representation for debugging.""" return f"{type(self).__name__}({str(self)!r})" - def __str__(self): + def __str__(self) -> str: """Format our JobsArgument class.""" return "auto" if self.is_auto else str(self.n_jobs) -def register_default_options(option_manager): +def register_default_options(option_manager: OptionManager) -> None: """Register the default options on our OptionManager. The default options include: diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 4d1d7b8..7509438 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -1,10 +1,18 @@ """Plugin built-in to Flake8 to treat pyflakes as a plugin.""" +import argparse +import ast import os +from typing import Any +from typing import Generator from typing import List +from typing import Tuple +from typing import Type import pyflakes.checker from flake8 import utils +from flake8.options.manager import OptionManager +from flake8.processor import _Token FLAKE8_PYFLAKES_CODES = { "UnusedImport": "F401", @@ -67,7 +75,9 @@ class FlakesChecker(pyflakes.checker.Checker): include_in_doctest: List[str] = [] exclude_from_doctest: List[str] = [] - def __init__(self, tree, file_tokens, filename): + def __init__( + self, tree: ast.AST, file_tokens: List[_Token], filename: str + ) -> None: """Initialize the PyFlakes plugin with an AST tree and filename.""" filename = utils.normalize_path(filename) with_doctest = self.with_doctest @@ -99,7 +109,7 @@ class FlakesChecker(pyflakes.checker.Checker): ) @classmethod - def add_options(cls, parser): + def add_options(cls, parser: OptionManager) -> None: """Register options for PyFlakes on the Flake8 OptionManager.""" parser.add_option( "--builtins", @@ -134,7 +144,7 @@ class FlakesChecker(pyflakes.checker.Checker): ) @classmethod - def parse_options(cls, options): + def parse_options(cls, options: argparse.Namespace) -> None: """Parse option values from Flake8's OptionManager.""" if options.builtins: cls.builtIns = cls.builtIns.union(options.builtins) @@ -171,7 +181,7 @@ class FlakesChecker(pyflakes.checker.Checker): f"both for doctesting." ) - def run(self): + def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]: """Run the plugin.""" for message in self.messages: col = getattr(message, "col", 0) diff --git a/tests/fixtures/config_files/local-plugin.ini b/tests/fixtures/config_files/local-plugin.ini index d0aa3be..8344f76 100644 --- a/tests/fixtures/config_files/local-plugin.ini +++ b/tests/fixtures/config_files/local-plugin.ini @@ -1,5 +1,5 @@ [flake8:local-plugins] extension = - XE = test_plugins:ExtensionTestPlugin + XE = tests.integration.test_plugins:ExtensionTestPlugin report = - XR = test_plugins:ReportTestPlugin + XR = tests.integration.test_plugins:ReportTestPlugin diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/subdir/__init__.py b/tests/integration/subdir/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_get_local_plugins.py b/tests/unit/test_get_local_plugins.py index 7e7b318..44d8725 100644 --- a/tests/unit/test_get_local_plugins.py +++ b/tests/unit/test_get_local_plugins.py @@ -41,5 +41,9 @@ def test_get_local_plugins(): localcfs.return_value = [config_fixture_path] local_plugins = config.get_local_plugins(config_finder) - assert local_plugins.extension == ["XE = test_plugins:ExtensionTestPlugin"] - assert local_plugins.report == ["XR = test_plugins:ReportTestPlugin"] + assert local_plugins.extension == [ + "XE = tests.integration.test_plugins:ExtensionTestPlugin" + ] + assert local_plugins.report == [ + "XR = tests.integration.test_plugins:ReportTestPlugin" + ] diff --git a/tests/unit/test_pyflakes_codes.py b/tests/unit/test_pyflakes_codes.py index 526832e..c251721 100644 --- a/tests/unit/test_pyflakes_codes.py +++ b/tests/unit/test_pyflakes_codes.py @@ -25,7 +25,7 @@ def f(): sys = sys """ tree = ast.parse(src) - checker = pyflakes_shim.FlakesChecker(tree, (), "t.py") + checker = pyflakes_shim.FlakesChecker(tree, [], "t.py") message_texts = [s for _, _, s, _ in checker.run()] assert message_texts == [ "F823 local variable 'sys' defined in enclosing scope on line 1 referenced before assignment", # noqa: E501 From 00ca6302c5391b99d84bdb92aac186cb86251cf3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 21:47:02 +0000 Subject: [PATCH 032/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.10b0 → 21.11b1](https://github.com/psf/black/compare/21.10b0...21.11b1) - [github.com/asottile/pyupgrade: v2.29.0 → v2.29.1](https://github.com/asottile/pyupgrade/compare/v2.29.0...v2.29.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bcbb585..5cd8601 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,12 +13,12 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/psf/black - rev: 21.10b0 + rev: 21.11b1 hooks: - id: black args: [--line-length=79] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade args: [--py36-plus] From 65c893728ec7834f95afdd06736107b4f3a5ea7a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 22 Nov 2021 19:42:50 -0500 Subject: [PATCH 033/257] refactor and simplify configuration loading --- docs/source/internal/option_handling.rst | 46 +-- src/flake8/api/legacy.py | 17 +- src/flake8/main/application.py | 68 ++-- src/flake8/main/options.py | 1 + src/flake8/options/aggregator.py | 36 +- src/flake8/options/config.py | 372 ++++-------------- src/flake8/options/manager.py | 2 +- tests/fixtures/config_files/README.rst | 10 - tests/fixtures/config_files/broken.ini | 9 - .../cli-specified-with-inline-comments.ini | 16 - .../cli-specified-without-inline-comments.ini | 16 - tests/fixtures/config_files/cli-specified.ini | 10 - .../config-with-hyphenated-options.ini | 5 - .../config_files/no-flake8-section.ini | 20 - tests/integration/test_aggregator.py | 44 ++- tests/unit/test_config_file_finder.py | 143 ------- tests/unit/test_config_parser.py | 188 --------- tests/unit/test_get_local_plugins.py | 49 --- tests/unit/test_legacy_api.py | 16 +- tests/unit/test_options_config.py | 166 ++++++++ 20 files changed, 351 insertions(+), 883 deletions(-) delete mode 100644 tests/fixtures/config_files/broken.ini delete mode 100644 tests/fixtures/config_files/cli-specified-with-inline-comments.ini delete mode 100644 tests/fixtures/config_files/cli-specified-without-inline-comments.ini delete mode 100644 tests/fixtures/config_files/cli-specified.ini delete mode 100644 tests/fixtures/config_files/config-with-hyphenated-options.ini delete mode 100644 tests/fixtures/config_files/no-flake8-section.ini delete mode 100644 tests/unit/test_config_file_finder.py delete mode 100644 tests/unit/test_config_parser.py delete mode 100644 tests/unit/test_get_local_plugins.py create mode 100644 tests/unit/test_options_config.py diff --git a/docs/source/internal/option_handling.rst b/docs/source/internal/option_handling.rst index 00c688f..9e8f4fd 100644 --- a/docs/source/internal/option_handling.rst +++ b/docs/source/internal/option_handling.rst @@ -41,7 +41,7 @@ three new parameters: The last two are not specifically for configuration file handling, but they do improve that dramatically. We found that there were options that, when -specified in a configuration file, often necessitated being spit +specified in a configuration file, often necessitated being split across multiple lines and those options were almost always comma-separated. For example, let's consider a user's list of ignored error codes for a project: @@ -157,42 +157,22 @@ problems with pep8's 1.6 series. As such, |Flake8| has separated out discovery, management, and merging into a module to make reasoning about each of these pieces easier and more explicit (as well as easier to test). -Configuration file discovery is managed by the -:class:`~flake8.options.config.ConfigFileFinder` object. This object needs to -know information about the program's name, any extra arguments passed to it, -and any configuration files that should be appended to the list of discovered -files. It provides methods for finding the files and similar methods for -parsing those fles. For example, it provides -:meth:`~flake8.options.config.ConfigFileFinder.local_config_files` to find -known local config files (and append the extra configuration files) and it -also provides :meth:`~flake8.options.config.ConfigFileFinder.local_configs` -to parse those configuration files. +Configuration file discovery and raw ini reading is managed by +:func:`~flake8.options.config.load_config`. This produces a loaded +:class:`~configparser.RawConfigParser` and a config directory (which will be +used later to normalize paths). -.. note:: ``local_config_files`` also filters out non-existent files. +Next, :func:`~flake8.options.config.parse_config` parses options using the +types in the ``OptionManager``. -Configuration file merging and managemnt is controlled by the -:class:`~flake8.options.config.ConfigParser`. This requires the instance -of :class:`~flake8.options.manager.OptionManager` that the program is using, -the list of appended config files, and the list of extra arguments. This -object is currently the sole user of the -:class:`~flake8.options.config.ConfigFileFinder` object. It appropriately -initializes the object and uses it in each of - -- :meth:`~flake8.options.config.ConfigParser.parse_cli_config` -- :meth:`~flake8.options.config.ConfigParser.parse_local_config` - -Finally, :meth:`~flake8.options.config.ConfigParser.parse` returns the -appropriate configuration dictionary for this execution of |Flake8|. The -main usage of the ``ConfigParser`` is in -:func:`~flake8.options.aggregator.aggregate_options`. +Most of this is done in :func:`~flake8.options.aggregator.aggregate_options`. Aggregating Configuration File and Command Line Arguments --------------------------------------------------------- :func:`~flake8.options.aggregator.aggregate_options` accepts an instance of :class:`~flake8.options.manager.OptionManager` and does the work to parse the -command-line arguments passed by the user necessary for creating an instance -of :class:`~flake8.options.config.ConfigParser`. +command-line arguments. After parsing the configuration file, we determine the default ignore list. We use the defaults from the OptionManager and update those with the parsed @@ -216,10 +196,6 @@ API Documentation :members: :special-members: -.. autoclass:: flake8.options.config.ConfigFileFinder - :members: - :special-members: +.. autofunction:: flake8.options.config.load_config -.. autoclass:: flake8.options.config.ConfigParser - :members: - :special-members: +.. autofunction:: flake8.options.config.parse_config diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index f80cb3d..ed54770 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -31,19 +31,16 @@ def get_style_guide(**kwargs): application = app.Application() prelim_opts, remaining_args = application.parse_preliminary_options([]) flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) - config_finder = config.ConfigFileFinder( - application.program, - prelim_opts.append_config, - config_file=prelim_opts.config, - ignore_config_files=prelim_opts.isolated, + + cfg, cfg_dir = config.load_config( + config=prelim_opts.config, + extra=prelim_opts.append_config, + isolated=prelim_opts.isolated, ) - application.find_plugins(config_finder) + application.find_plugins(cfg, cfg_dir) application.register_plugin_options() - application.parse_configuration_and_cli( - config_finder, - remaining_args, - ) + application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) # We basically want application.initialize to be called but with these # options set instead before we make our formatter, notifier, internal # style guide and file checker manager. diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 2ed2f68..6825f91 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -1,5 +1,6 @@ """Module containing the application logic for Flake8.""" import argparse +import configparser import logging import sys import time @@ -130,24 +131,35 @@ class Application: else: return int((self.result_count > 0) or self.catastrophic_failure) - def find_plugins(self, config_finder: config.ConfigFileFinder) -> None: + def find_plugins( + self, + cfg: configparser.RawConfigParser, + cfg_dir: str, + ) -> None: """Find and load the plugins for this application. Set the :attr:`check_plugins` and :attr:`formatting_plugins` attributes based on the discovered plugins found. - - :param config.ConfigFileFinder config_finder: - The finder for finding and reading configuration files. """ - local_plugins = config.get_local_plugins(config_finder) - - sys.path.extend(local_plugins.paths) - - self.check_plugins = plugin_manager.Checkers(local_plugins.extension) - - self.formatting_plugins = plugin_manager.ReportFormatters( - local_plugins.report + # TODO: move to src/flake8/plugins/finder.py + extension_local = utils.parse_comma_separated_list( + cfg.get("flake8:local-plugins", "extension", fallback="").strip(), + regexp=utils.LOCAL_PLUGIN_LIST_RE, ) + report_local = utils.parse_comma_separated_list( + cfg.get("flake8:local-plugins", "report", fallback="").strip(), + regexp=utils.LOCAL_PLUGIN_LIST_RE, + ) + + paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() + local_paths = utils.parse_comma_separated_list(paths_s) + local_paths = utils.normalize_paths(local_paths, cfg_dir) + + sys.path.extend(local_paths) + + self.check_plugins = plugin_manager.Checkers(extension_local) + + self.formatting_plugins = plugin_manager.ReportFormatters(report_local) self.check_plugins.load_plugins() self.formatting_plugins.load_plugins() @@ -162,19 +174,15 @@ class Application: def parse_configuration_and_cli( self, - config_finder: config.ConfigFileFinder, + cfg: configparser.RawConfigParser, + cfg_dir: str, argv: List[str], ) -> None: - """Parse configuration files and the CLI options. - - :param config.ConfigFileFinder config_finder: - The finder for finding and reading configuration files. - :param list argv: - Command-line arguments passed in directly. - """ + """Parse configuration files and the CLI options.""" self.options = aggregator.aggregate_options( self.option_manager, - config_finder, + cfg, + cfg_dir, argv, ) @@ -329,18 +337,16 @@ class Application: # our legacy API calls to these same methods. prelim_opts, remaining_args = self.parse_preliminary_options(argv) flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) - config_finder = config.ConfigFileFinder( - self.program, - prelim_opts.append_config, - config_file=prelim_opts.config, - ignore_config_files=prelim_opts.isolated, + + cfg, cfg_dir = config.load_config( + config=prelim_opts.config, + extra=prelim_opts.append_config, + isolated=prelim_opts.isolated, ) - self.find_plugins(config_finder) + + self.find_plugins(cfg, cfg_dir) self.register_plugin_options() - self.parse_configuration_and_cli( - config_finder, - remaining_args, - ) + self.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) self.make_formatter() self.make_guide() self.make_file_checker_manager() diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index babd44b..3abc043 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -39,6 +39,7 @@ def register_preliminary_options(parser: argparse.ArgumentParser) -> None: add_argument( "--append-config", action="append", + default=[], help="Provide extra config files to parse in addition to the files " "found by Flake8 by default. These files are the last ones read " "and so they take the highest precedence when multiple files " diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index 6458d69..0311257 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -4,8 +4,10 @@ This holds the logic that uses the collected and merged config files and applies the user-specified command-line configuration on top of it. """ import argparse +import configparser import logging -from typing import List +from typing import Optional +from typing import Sequence from flake8.options import config from flake8.options.manager import OptionManager @@ -15,34 +17,16 @@ LOG = logging.getLogger(__name__) def aggregate_options( manager: OptionManager, - config_finder: config.ConfigFileFinder, - argv: List[str], + cfg: configparser.RawConfigParser, + cfg_dir: str, + argv: Optional[Sequence[str]], ) -> argparse.Namespace: - """Aggregate and merge CLI and config file options. - - :param flake8.options.manager.OptionManager manager: - The instance of the OptionManager that we're presently using. - :param flake8.options.config.ConfigFileFinder config_finder: - The config file finder to use. - :param list argv: - The list of remaining command-line arguments that were unknown during - preliminary option parsing to pass to ``manager.parse_args``. - :returns: - Tuple of the parsed options and extra arguments returned by - ``manager.parse_args``. - :rtype: - tuple(argparse.Namespace, list) - """ + """Aggregate and merge CLI and config file options.""" # Get defaults from the option parser default_values = manager.parse_args([]) - # Make our new configuration file mergerator - config_parser = config.ConfigParser( - option_manager=manager, config_finder=config_finder - ) - # Get the parsed config - parsed_config = config_parser.parse() + parsed_config = config.parse_config(manager, cfg, cfg_dir) # Extend the default ignore value with the extended default ignore list, # registered by plugins. @@ -70,7 +54,9 @@ def aggregate_options( # If the config name is somehow different from the destination name, # fetch the destination name from our Option if not hasattr(default_values, config_name): - dest_name = config_parser.config_options[config_name].dest + dest_val = manager.config_options_dict[config_name].dest + assert isinstance(dest_val, str) + dest_name = dest_val LOG.debug( 'Overriding default value of (%s) for "%s" with (%s)', diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index fc3b205..d7519df 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -1,318 +1,108 @@ """Config handling logic for Flake8.""" -import collections import configparser import logging import os.path +from typing import Any +from typing import Dict from typing import List from typing import Optional from typing import Tuple -from flake8 import utils +from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) -__all__ = ("ConfigFileFinder", "ConfigParser") - -class ConfigFileFinder: - """Encapsulate the logic for finding and reading config files.""" - - def __init__( - self, - program_name: str, - extra_config_files: Optional[List[str]] = None, - config_file: Optional[str] = None, - ignore_config_files: bool = False, - ) -> None: - """Initialize object to find config files. - - :param str program_name: - Name of the current program (e.g., flake8). - :param list extra_config_files: - Extra configuration files specified by the user to read. - :param str config_file: - Configuration file override to only read configuration from. - :param bool ignore_config_files: - Determine whether to ignore configuration files or not. - """ - # The values of --append-config from the CLI - if extra_config_files is None: - extra_config_files = [] - self.extra_config_files = utils.normalize_paths(extra_config_files) - - # The value of --config from the CLI. - self.config_file = config_file - - # The value of --isolated from the CLI. - self.ignore_config_files = ignore_config_files - - # User configuration file. - self.program_name = program_name - - # List of filenames to find in the local/project directory - self.project_filenames = ("setup.cfg", "tox.ini", f".{program_name}") - - self.local_directory = os.path.abspath(os.curdir) - - @staticmethod - def _read_config( - *files: str, - ) -> Tuple[configparser.RawConfigParser, List[str]]: - config = configparser.RawConfigParser() - - found_files = [] - for filename in files: +def _find_config_file(path: str) -> Optional[str]: + cfg = configparser.RawConfigParser() + while True: + for candidate in ("setup.cfg", "tox.ini", ".flake8"): + cfg_path = os.path.join(path, candidate) try: - found_files.extend(config.read(filename)) - except UnicodeDecodeError: - LOG.exception( - "There was an error decoding a config file." - "The file with a problem was %s.", - filename, - ) - except configparser.ParsingError: - LOG.exception( - "There was an error trying to parse a config " - "file. The file with a problem was %s.", - filename, - ) - return (config, found_files) + cfg.read(cfg_path) + except (UnicodeDecodeError, configparser.ParsingError) as e: + LOG.warning("ignoring unparseable config %s: %s", cfg_path, e) + else: + # only consider it a config if it contains flake8 sections + if "flake8" in cfg or "flake8:local-plugins" in cfg: + return cfg_path - def cli_config(self, files: str) -> configparser.RawConfigParser: - """Read and parse the config file specified on the command-line.""" - config, found_files = self._read_config(files) - if found_files: - LOG.debug("Found cli configuration files: %s", found_files) - return config + new_path = os.path.dirname(path) + if new_path == path: + break + else: + path = new_path - def generate_possible_local_files(self): - """Find and generate all local config files.""" - parent = tail = os.getcwd() - found_config_files = False - while tail and not found_config_files: - for project_filename in self.project_filenames: - filename = os.path.abspath( - os.path.join(parent, project_filename) - ) - if os.path.exists(filename): - yield filename - found_config_files = True - self.local_directory = parent - (parent, tail) = os.path.split(parent) - - def local_config_files(self): - """Find all local config files which actually exist. - - Filter results from - :meth:`~ConfigFileFinder.generate_possible_local_files` based - on whether the filename exists or not. - - :returns: - List of files that exist that are local project config files with - extra config files appended to that list (which also exist). - :rtype: - [str] - """ - exists = os.path.exists - return [ - filename for filename in self.generate_possible_local_files() - ] + [f for f in self.extra_config_files if exists(f)] - - def local_configs_with_files(self): - """Parse all local config files into one config object. - - Return (config, found_config_files) tuple. - """ - config, found_files = self._read_config(*self.local_config_files()) - if found_files: - LOG.debug("Found local configuration files: %s", found_files) - return (config, found_files) - - def local_configs(self): - """Parse all local config files into one config object.""" - return self.local_configs_with_files()[0] + # did not find any configuration file + return None -class ConfigParser: - """Encapsulate merging different types of configuration files. +def load_config( + config: Optional[str], + extra: List[str], + *, + isolated: bool = False, +) -> Tuple[configparser.RawConfigParser, str]: + """Load the configuration given the user options. - This parses out the options registered that were specified in the - configuration files, handles extra configuration files, and returns - dictionaries with the parsed values. + - in ``isolated`` mode, return an empty configuration + - if a config file is given in ``config`` use that, otherwise attempt to + discover a configuration using ``tox.ini`` / ``setup.cfg`` / ``.flake8`` + - finally, load any ``extra`` configuration files """ + pwd = os.path.abspath(".") - #: Set of actions that should use the - #: :meth:`~configparser.RawConfigParser.getbool` method. - GETBOOL_ACTIONS = {"store_true", "store_false"} + if isolated: + return configparser.RawConfigParser(), pwd - def __init__(self, option_manager, config_finder): - """Initialize the ConfigParser instance. + if config is None: + config = _find_config_file(pwd) - :param flake8.options.manager.OptionManager option_manager: - Initialized OptionManager. - :param flake8.options.config.ConfigFileFinder config_finder: - Initialized ConfigFileFinder. - """ - #: Our instance of flake8.options.manager.OptionManager - self.option_manager = option_manager - #: The prog value for the cli parser - self.program_name = option_manager.program_name - #: Mapping of configuration option names to - #: :class:`~flake8.options.manager.Option` instances - self.config_options = option_manager.config_options_dict - #: Our instance of our :class:`~ConfigFileFinder` - self.config_finder = config_finder - - def _normalize_value(self, option, value, parent=None): - if parent is None: - parent = self.config_finder.local_directory - - final_value = option.normalize(value, parent) - LOG.debug( - '%r has been normalized to %r for option "%s"', - value, - final_value, - option.config_name, - ) - return final_value - - def _parse_config(self, config_parser, parent=None): - config_dict = {} - for option_name in config_parser.options(self.program_name): - if option_name not in self.config_options: - LOG.debug( - 'Option "%s" is not registered. Ignoring.', option_name - ) - continue - option = self.config_options[option_name] - - # Use the appropriate method to parse the config value - method = config_parser.get - if option.type is int or option.action == "count": - method = config_parser.getint - elif option.action in self.GETBOOL_ACTIONS: - method = config_parser.getboolean - - value = method(self.program_name, option_name) - LOG.debug('Option "%s" returned value: %r', option_name, value) - - final_value = self._normalize_value(option, value, parent) - config_dict[option.config_name] = final_value - - return config_dict - - def is_configured_by(self, config): - """Check if the specified config parser has an appropriate section.""" - return config.has_section(self.program_name) - - def parse_local_config(self): - """Parse and return the local configuration files.""" - config = self.config_finder.local_configs() - if not self.is_configured_by(config): - LOG.debug( - "Local configuration files have no %s section", - self.program_name, - ) - return {} - - LOG.debug("Parsing local configuration files.") - return self._parse_config(config) - - def parse_cli_config(self, config_path): - """Parse and return the file specified by --config.""" - config = self.config_finder.cli_config(config_path) - if not self.is_configured_by(config): - LOG.debug( - "CLI configuration files have no %s section", - self.program_name, - ) - return {} - - LOG.debug("Parsing CLI configuration files.") - return self._parse_config(config, os.path.dirname(config_path)) - - def parse(self): - """Parse and return the local config files. - - :returns: - Dictionary of parsed configuration options - :rtype: - dict - """ - if self.config_finder.ignore_config_files: - LOG.debug( - "Refusing to parse configuration files due to user-" - "requested isolation" - ) - return {} - - if self.config_finder.config_file: - LOG.debug( - "Ignoring user and locally found configuration files. " - 'Reading only configuration from "%s" specified via ' - "--config by the user", - self.config_finder.config_file, - ) - return self.parse_cli_config(self.config_finder.config_file) - - return self.parse_local_config() - - -def get_local_plugins(config_finder): - """Get local plugins lists from config files. - - :param flake8.options.config.ConfigFileFinder config_finder: - The config file finder to use. - :returns: - LocalPlugins namedtuple containing two lists of plugin strings, - one for extension (checker) plugins and one for report plugins. - :rtype: - flake8.options.config.LocalPlugins - """ - local_plugins = LocalPlugins(extension=[], report=[], paths=[]) - if config_finder.ignore_config_files: - LOG.debug( - "Refusing to look for local plugins in configuration" - "files due to user-requested isolation" - ) - return local_plugins - - if config_finder.config_file: - LOG.debug( - 'Reading local plugins only from "%s" specified via ' - "--config by the user", - config_finder.config_file, - ) - config = config_finder.cli_config(config_finder.config_file) - config_files = [config_finder.config_file] + cfg = configparser.RawConfigParser() + if config is not None: + cfg.read(config) + cfg_dir = os.path.dirname(config) else: - config, config_files = config_finder.local_configs_with_files() + cfg_dir = pwd - base_dirs = {os.path.dirname(cf) for cf in config_files} + # TODO: remove this and replace it with configuration modifying plugins + # read the additional configs afterwards + for filename in extra: + cfg.read(filename) - section = f"{config_finder.program_name}:local-plugins" - for plugin_type in ["extension", "report"]: - if config.has_option(section, plugin_type): - local_plugins_string = config.get(section, plugin_type).strip() - plugin_type_list = getattr(local_plugins, plugin_type) - plugin_type_list.extend( - utils.parse_comma_separated_list( - local_plugins_string, regexp=utils.LOCAL_PLUGIN_LIST_RE - ) - ) - if config.has_option(section, "paths"): - raw_paths = utils.parse_comma_separated_list( - config.get(section, "paths").strip() - ) - norm_paths: List[str] = [] - for base_dir in base_dirs: - norm_paths.extend( - path - for path in utils.normalize_paths(raw_paths, parent=base_dir) - if os.path.exists(path) - ) - local_plugins.paths.extend(norm_paths) - return local_plugins + return cfg, cfg_dir -LocalPlugins = collections.namedtuple("LocalPlugins", "extension report paths") +def parse_config( + option_manager: OptionManager, + cfg: configparser.RawConfigParser, + cfg_dir: str, +) -> Dict[str, Any]: + """Parse and normalize the typed configuration options.""" + if "flake8" not in cfg: + return {} + + config_dict = {} + + for option_name in cfg["flake8"]: + option = option_manager.config_options_dict.get(option_name) + if option is None: + LOG.debug('Option "%s" is not registered. Ignoring.', option_name) + continue + + # Use the appropriate method to parse the config value + value: Any + if option.type is int or option.action == "count": + value = cfg.getint("flake8", option_name) + elif option.action in {"store_true", "store_false"}: + value = cfg.getboolean("flake8", option_name) + else: + value = cfg.get("flake8", option_name) + + LOG.debug('Option "%s" returned value: %r', option_name, value) + + final_value = option.normalize(value, cfg_dir) + assert option.config_name is not None + config_dict[option.config_name] = final_value + + return config_dict diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index f9765e1..a125372 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -462,7 +462,7 @@ class OptionManager: def parse_args( self, - args: Optional[List[str]] = None, + args: Optional[Sequence[str]] = None, values: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: """Proxy to calling the OptionParser's parse_args method.""" diff --git a/tests/fixtures/config_files/README.rst b/tests/fixtures/config_files/README.rst index 4570989..8796051 100644 --- a/tests/fixtures/config_files/README.rst +++ b/tests/fixtures/config_files/README.rst @@ -16,17 +16,7 @@ Files that should not be created Purposes of existing fixtures ----------------------------- -``tests/fixtures/config_files/cli-specified.ini`` - - This should only be used when providing config file(s) specified by the - user on the command-line. - ``tests/fixtures/config_files/local-plugin.ini`` This is for testing configuring a plugin via flake8 config file instead of setuptools entry-point. - -``tests/fixtures/config_files/no-flake8-section.ini`` - - This should be used when parsing an ini file without a ``[flake8]`` - section. diff --git a/tests/fixtures/config_files/broken.ini b/tests/fixtures/config_files/broken.ini deleted file mode 100644 index 33986ae..0000000 --- a/tests/fixtures/config_files/broken.ini +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -exclude = -<<<<<<< 642f88cb1b6027e184d9a662b255f7fea4d9eacc - tests/fixtures/, -======= - tests/, ->>>>>>> HEAD - docs/ -ignore = D203 diff --git a/tests/fixtures/config_files/cli-specified-with-inline-comments.ini b/tests/fixtures/config_files/cli-specified-with-inline-comments.ini deleted file mode 100644 index 4d57e85..0000000 --- a/tests/fixtures/config_files/cli-specified-with-inline-comments.ini +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -# This is a flake8 config, there are many like it, but this is mine -ignore = - # Disable E123 - E123, - # Disable W234 - W234, - # Also disable E111 - E111 -exclude = - # Exclude foo/ - foo/, - # Exclude bar/ while we're at it - bar/, - # Exclude bogus/ - bogus/ diff --git a/tests/fixtures/config_files/cli-specified-without-inline-comments.ini b/tests/fixtures/config_files/cli-specified-without-inline-comments.ini deleted file mode 100644 index f50ba75..0000000 --- a/tests/fixtures/config_files/cli-specified-without-inline-comments.ini +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -# This is a flake8 config, there are many like it, but this is mine -# Disable E123 -# Disable W234 -# Also disable E111 -ignore = - E123, - W234, - E111 -# Exclude foo/ -# Exclude bar/ while we're at it -# Exclude bogus/ -exclude = - foo/, - bar/, - bogus/ diff --git a/tests/fixtures/config_files/cli-specified.ini b/tests/fixtures/config_files/cli-specified.ini deleted file mode 100644 index 75c5f23..0000000 --- a/tests/fixtures/config_files/cli-specified.ini +++ /dev/null @@ -1,10 +0,0 @@ -[flake8] -ignore = - E123, - W234, - E111 -exclude = - foo/, - bar/, - bogus/ -quiet = 1 diff --git a/tests/fixtures/config_files/config-with-hyphenated-options.ini b/tests/fixtures/config_files/config-with-hyphenated-options.ini deleted file mode 100644 index cc0f90e..0000000 --- a/tests/fixtures/config_files/config-with-hyphenated-options.ini +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 110 -enable_extensions = - H101, - H235 diff --git a/tests/fixtures/config_files/no-flake8-section.ini b/tests/fixtures/config_files/no-flake8-section.ini deleted file mode 100644 index a85b709..0000000 --- a/tests/fixtures/config_files/no-flake8-section.ini +++ /dev/null @@ -1,20 +0,0 @@ -[tox] -minversion=2.3.1 -envlist = py26,py27,py32,py33,py34,py35,flake8 - -[testenv] -deps = - mock - pytest -commands = - py.test {posargs} - -[testenv:flake8] -skipsdist = true -skip_install = true -use_develop = false -deps = - flake8 - flake8-docstrings -commands = - flake8 diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py index 70331a4..cdc7281 100644 --- a/tests/integration/test_aggregator.py +++ b/tests/integration/test_aggregator.py @@ -1,5 +1,4 @@ """Test aggregation of config files and command-line options.""" -import argparse import os import pytest @@ -9,24 +8,38 @@ from flake8.options import aggregator from flake8.options import config from flake8.options import manager -CLI_SPECIFIED_CONFIG = "tests/fixtures/config_files/cli-specified.ini" - @pytest.fixture def optmanager(): """Create a new OptionManager.""" - prelim_parser = argparse.ArgumentParser(add_help=False) - options.register_preliminary_options(prelim_parser) option_manager = manager.OptionManager( prog="flake8", version="3.0.0", - parents=[prelim_parser], ) options.register_default_options(option_manager) return option_manager -def test_aggregate_options_with_config(optmanager): +@pytest.fixture +def flake8_config(tmp_path): + cfg_s = """\ +[flake8] +ignore = + E123, + W234, + E111 +exclude = + foo/, + bar/, + bogus/ +quiet = 1 +""" + cfg = tmp_path.joinpath("tox.ini") + cfg.write_text(cfg_s) + return str(cfg) + + +def test_aggregate_options_with_config(optmanager, flake8_config): """Verify we aggregate options and config values appropriately.""" arguments = [ "flake8", @@ -35,11 +48,12 @@ def test_aggregate_options_with_config(optmanager): "--exclude", "tests/*", ] - config_finder = config.ConfigFileFinder( - "flake8", config_file=CLI_SPECIFIED_CONFIG - ) + cfg, cfg_dir = config.load_config(flake8_config, []) options = aggregator.aggregate_options( - optmanager, config_finder, arguments + optmanager, + cfg, + cfg_dir, + arguments, ) assert options.select == ["E11", "E34", "E402", "W", "F"] @@ -47,7 +61,7 @@ def test_aggregate_options_with_config(optmanager): assert options.exclude == [os.path.abspath("tests/*")] -def test_aggregate_options_when_isolated(optmanager): +def test_aggregate_options_when_isolated(optmanager, flake8_config): """Verify we aggregate options and config values appropriately.""" arguments = [ "flake8", @@ -56,11 +70,9 @@ def test_aggregate_options_when_isolated(optmanager): "--exclude", "tests/*", ] - config_finder = config.ConfigFileFinder("flake8", ignore_config_files=True) + cfg, cfg_dir = config.load_config(flake8_config, [], isolated=True) optmanager.extend_default_ignore(["E8"]) - options = aggregator.aggregate_options( - optmanager, config_finder, arguments - ) + options = aggregator.aggregate_options(optmanager, cfg, cfg_dir, arguments) assert options.select == ["E11", "E34", "E402", "W", "F"] assert sorted(options.ignore) == [ diff --git a/tests/unit/test_config_file_finder.py b/tests/unit/test_config_file_finder.py deleted file mode 100644 index 5116796..0000000 --- a/tests/unit/test_config_file_finder.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Tests for the ConfigFileFinder.""" -import configparser -import os -from unittest import mock - -import pytest - -from flake8.options import config - -CLI_SPECIFIED_FILEPATH = "tests/fixtures/config_files/cli-specified.ini" -BROKEN_CONFIG_PATH = "tests/fixtures/config_files/broken.ini" - - -def test_cli_config(): - """Verify opening and reading the file specified via the cli.""" - cli_filepath = CLI_SPECIFIED_FILEPATH - finder = config.ConfigFileFinder("flake8") - - parsed_config = finder.cli_config(cli_filepath) - assert parsed_config.has_section("flake8") - - -@pytest.mark.parametrize( - "cwd,expected", - [ - # Root directory of project - ( - os.path.abspath("."), - [os.path.abspath("setup.cfg"), os.path.abspath("tox.ini")], - ), - # Subdirectory of project directory - ( - os.path.abspath("src"), - [os.path.abspath("setup.cfg"), os.path.abspath("tox.ini")], - ), - # Outside of project directory - (os.path.abspath("/"), []), - ], -) -def test_generate_possible_local_files(cwd, expected): - """Verify generation of all possible config paths.""" - finder = config.ConfigFileFinder("flake8") - - with mock.patch.object(os, "getcwd", return_value=cwd): - config_files = list(finder.generate_possible_local_files()) - - assert config_files == expected - - -@pytest.mark.parametrize( - "extra_config_files,expected", - [ - # Extra config files specified - ( - [CLI_SPECIFIED_FILEPATH], - [ - os.path.abspath("setup.cfg"), - os.path.abspath("tox.ini"), - os.path.abspath(CLI_SPECIFIED_FILEPATH), - ], - ), - # Missing extra config files specified - ( - [ - CLI_SPECIFIED_FILEPATH, - "tests/fixtures/config_files/missing.ini", - ], - [ - os.path.abspath("setup.cfg"), - os.path.abspath("tox.ini"), - os.path.abspath(CLI_SPECIFIED_FILEPATH), - ], - ), - ], -) -def test_local_config_files(extra_config_files, expected): - """Verify discovery of local config files.""" - finder = config.ConfigFileFinder("flake8", extra_config_files) - - assert list(finder.local_config_files()) == expected - - -def test_local_configs(): - """Verify we return a ConfigParser.""" - finder = config.ConfigFileFinder("flake8") - - assert isinstance(finder.local_configs(), configparser.RawConfigParser) - - -@pytest.mark.parametrize( - "files", - [ - [BROKEN_CONFIG_PATH], - [CLI_SPECIFIED_FILEPATH, BROKEN_CONFIG_PATH], - ], -) -def test_read_config_catches_broken_config_files(files): - """Verify that we do not allow the exception to bubble up.""" - _, parsed = config.ConfigFileFinder._read_config(*files) - assert BROKEN_CONFIG_PATH not in parsed - - -def test_read_config_catches_decoding_errors(tmpdir): - """Verify that we do not allow the exception to bubble up.""" - setup_cfg = tmpdir.join("setup.cfg") - # pick bytes that are unlikely to decode - setup_cfg.write_binary(b"[x]\ny = \x81\x8d\x90\x9d") - _, parsed = config.ConfigFileFinder._read_config(setup_cfg.strpath) - assert parsed == [] - - -def test_config_file_default_value(): - """Verify the default 'config_file' attribute value.""" - finder = config.ConfigFileFinder("flake8") - assert finder.config_file is None - - -def test_setting_config_file_value(): - """Verify the 'config_file' attribute matches constructed value.""" - config_file_value = "flake8.ini" - finder = config.ConfigFileFinder("flake8", config_file=config_file_value) - assert finder.config_file == config_file_value - - -def test_ignore_config_files_default_value(): - """Verify the default 'ignore_config_files' attribute value.""" - finder = config.ConfigFileFinder("flake8") - assert finder.ignore_config_files is False - - -@pytest.mark.parametrize( - "ignore_config_files_arg", - [ - False, - True, - ], -) -def test_setting_ignore_config_files_value(ignore_config_files_arg): - """Verify the 'ignore_config_files' attribute matches constructed value.""" - finder = config.ConfigFileFinder( - "flake8", ignore_config_files=ignore_config_files_arg - ) - assert finder.ignore_config_files is ignore_config_files_arg diff --git a/tests/unit/test_config_parser.py b/tests/unit/test_config_parser.py deleted file mode 100644 index 0baa108..0000000 --- a/tests/unit/test_config_parser.py +++ /dev/null @@ -1,188 +0,0 @@ -"""Unit tests for flake8.options.config.ConfigParser.""" -import os -from unittest import mock - -import pytest - -from flake8.options import config -from flake8.options import manager - - -@pytest.fixture -def optmanager(): - """Generate an OptionManager with simple values.""" - return manager.OptionManager(prog="flake8", version="3.0.0a1") - - -@pytest.fixture -def config_finder(): - """Generate a simple ConfigFileFinder.""" - return config.ConfigFileFinder("flake8") - - -def test_parse_cli_config(optmanager, config_finder): - """Parse the specified config file as a cli config file.""" - optmanager.add_option( - "--exclude", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - ) - optmanager.add_option( - "--ignore", parse_from_config=True, comma_separated_list=True - ) - optmanager.add_option("--quiet", parse_from_config=True, action="count") - parser = config.ConfigParser(optmanager, config_finder) - - config_file = "tests/fixtures/config_files/cli-specified.ini" - parsed_config = parser.parse_cli_config(config_file) - - config_dir = os.path.dirname(config_file) - assert parsed_config == { - "ignore": ["E123", "W234", "E111"], - "exclude": [ - os.path.abspath(os.path.join(config_dir, "foo/")), - os.path.abspath(os.path.join(config_dir, "bar/")), - os.path.abspath(os.path.join(config_dir, "bogus/")), - ], - "quiet": 1, - } - - -@pytest.mark.parametrize( - "filename,is_configured_by", - [ - ("tests/fixtures/config_files/cli-specified.ini", True), - ("tests/fixtures/config_files/no-flake8-section.ini", False), - ], -) -def test_is_configured_by( - filename, is_configured_by, optmanager, config_finder -): - """Verify the behaviour of the is_configured_by method.""" - parsed_config, _ = config.ConfigFileFinder._read_config(filename) - parser = config.ConfigParser(optmanager, config_finder) - - assert parser.is_configured_by(parsed_config) is is_configured_by - - -def test_parse_local_config(optmanager, config_finder): - """Verify parsing of local config files.""" - optmanager.add_option( - "--exclude", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - ) - optmanager.add_option( - "--ignore", parse_from_config=True, comma_separated_list=True - ) - optmanager.add_option("--quiet", parse_from_config=True, action="count") - parser = config.ConfigParser(optmanager, config_finder) - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [ - "tests/fixtures/config_files/cli-specified.ini" - ] - parsed_config = parser.parse_local_config() - - assert parsed_config == { - "ignore": ["E123", "W234", "E111"], - "exclude": [ - os.path.abspath("foo/"), - os.path.abspath("bar/"), - os.path.abspath("bogus/"), - ], - "quiet": 1, - } - - -def test_parse_isolates_config(optmanager): - """Verify behaviour of the parse method with isolated=True.""" - config_finder = mock.MagicMock() - config_finder.ignore_config_files = True - parser = config.ConfigParser(optmanager, config_finder) - - assert parser.parse() == {} - assert config_finder.local_configs.called is False - - -def test_parse_uses_cli_config(optmanager): - """Verify behaviour of the parse method with a specified config.""" - config_file_value = "foo.ini" - config_finder = mock.MagicMock() - config_finder.config_file = config_file_value - config_finder.ignore_config_files = False - parser = config.ConfigParser(optmanager, config_finder) - - parser.parse() - config_finder.cli_config.assert_called_once_with(config_file_value) - - -@pytest.mark.parametrize( - "config_fixture_path", - [ - "tests/fixtures/config_files/cli-specified.ini", - "tests/fixtures/config_files/cli-specified-with-inline-comments.ini", - "tests/fixtures/config_files/cli-specified-without-inline-comments.ini", # noqa: E501 - ], -) -def test_parsed_configs_are_equivalent( - optmanager, config_finder, config_fixture_path -): - """Verify the each file matches the expected parsed output. - - This is used to ensure our documented behaviour does not regress. - """ - optmanager.add_option( - "--exclude", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - ) - optmanager.add_option( - "--ignore", parse_from_config=True, comma_separated_list=True - ) - parser = config.ConfigParser(optmanager, config_finder) - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [config_fixture_path] - parsed_config = parser.parse() - - assert parsed_config["ignore"] == ["E123", "W234", "E111"] - assert parsed_config["exclude"] == [ - os.path.abspath("foo/"), - os.path.abspath("bar/"), - os.path.abspath("bogus/"), - ] - - -@pytest.mark.parametrize( - "config_file", - ["tests/fixtures/config_files/config-with-hyphenated-options.ini"], -) -def test_parsed_hyphenated_and_underscored_names( - optmanager, config_finder, config_file -): - """Verify we find hyphenated option names as well as underscored. - - This tests for options like --max-line-length and --enable-extensions - which are able to be specified either as max-line-length or - max_line_length in our config files. - """ - optmanager.add_option( - "--max-line-length", parse_from_config=True, type=int - ) - optmanager.add_option( - "--enable-extensions", - parse_from_config=True, - comma_separated_list=True, - ) - parser = config.ConfigParser(optmanager, config_finder) - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [config_file] - parsed_config = parser.parse() - - assert parsed_config["max_line_length"] == 110 - assert parsed_config["enable_extensions"] == ["H101", "H235"] diff --git a/tests/unit/test_get_local_plugins.py b/tests/unit/test_get_local_plugins.py deleted file mode 100644 index 44d8725..0000000 --- a/tests/unit/test_get_local_plugins.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Tests for get_local_plugins.""" -from unittest import mock - -from flake8.options import config - - -def test_get_local_plugins_respects_isolated(): - """Verify behaviour of get_local_plugins with isolated=True.""" - config_finder = mock.MagicMock() - config_finder.ignore_config_files = True - - local_plugins = config.get_local_plugins(config_finder) - - assert local_plugins.extension == [] - assert local_plugins.report == [] - assert config_finder.local_configs.called is False - assert config_finder.user_config.called is False - - -def test_get_local_plugins_uses_cli_config(): - """Verify behaviour of get_local_plugins with a specified config.""" - config_obj = mock.Mock() - config_finder = mock.MagicMock() - config_finder.cli_config.return_value = config_obj - config_finder.ignore_config_files = False - config_obj.get.return_value = "" - config_file_value = "foo.ini" - config_finder.config_file = config_file_value - - config.get_local_plugins(config_finder) - - config_finder.cli_config.assert_called_once_with(config_file_value) - - -def test_get_local_plugins(): - """Verify get_local_plugins returns expected plugins.""" - config_fixture_path = "tests/fixtures/config_files/local-plugin.ini" - config_finder = config.ConfigFileFinder("flake8") - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [config_fixture_path] - local_plugins = config.get_local_plugins(config_finder) - - assert local_plugins.extension == [ - "XE = tests.integration.test_plugins:ExtensionTestPlugin" - ] - assert local_plugins.report == [ - "XR = tests.integration.test_plugins:ReportTestPlugin" - ] diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 1dcdeb6..671b21a 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -1,5 +1,6 @@ """Tests for Flake8's legacy API.""" import argparse +import configparser import os.path from unittest import mock @@ -7,7 +8,7 @@ import pytest from flake8.api import legacy as api from flake8.formatting import base as formatter -from flake8.options.config import ConfigFileFinder +from flake8.options import config def test_get_style_guide(): @@ -22,22 +23,21 @@ def test_get_style_guide(): mockedapp = mock.Mock() mockedapp.parse_preliminary_options.return_value = (prelim_opts, []) mockedapp.program = "flake8" - with mock.patch( - "flake8.api.legacy.config.ConfigFileFinder" - ) as mock_config_finder: # noqa: E501 - config_finder = ConfigFileFinder(mockedapp.program) - mock_config_finder.return_value = config_finder + cfg = configparser.RawConfigParser() + cfg_dir = os.getcwd() + + with mock.patch.object(config, "load_config", return_value=(cfg, cfg_dir)): with mock.patch("flake8.main.application.Application") as application: application.return_value = mockedapp style_guide = api.get_style_guide() application.assert_called_once_with() mockedapp.parse_preliminary_options.assert_called_once_with([]) - mockedapp.find_plugins.assert_called_once_with(config_finder) + mockedapp.find_plugins.assert_called_once_with(cfg, cfg_dir) mockedapp.register_plugin_options.assert_called_once_with() mockedapp.parse_configuration_and_cli.assert_called_once_with( - config_finder, [] + cfg, cfg_dir, [] ) mockedapp.make_formatter.assert_called_once_with() mockedapp.make_guide.assert_called_once_with() diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py new file mode 100644 index 0000000..75bfac5 --- /dev/null +++ b/tests/unit/test_options_config.py @@ -0,0 +1,166 @@ +import configparser + +import pytest + +from flake8.main.options import register_default_options +from flake8.options import config +from flake8.options.manager import OptionManager + + +def test_config_not_found_returns_none(tmp_path): + assert config._find_config_file(str(tmp_path)) is None + + +def test_config_file_without_section_is_not_considered(tmp_path): + tmp_path.joinpath("setup.cfg").touch() + + assert config._find_config_file(str(tmp_path)) is None + + +def test_config_file_with_parse_error_is_not_considered(tmp_path, caplog): + tmp_path.joinpath("setup.cfg").write_text("[error") + + assert config._find_config_file(str(tmp_path)) is None + + assert len(caplog.record_tuples) == 1 + ((mod, level, msg),) = caplog.record_tuples + assert (mod, level) == ("flake8.options.config", 30) + assert msg.startswith("ignoring unparseable config ") + + +def test_config_file_with_encoding_error_is_not_considered(tmp_path, caplog): + tmp_path.joinpath("setup.cfg").write_bytes(b"\xa0\xef\xfe\x12") + + assert config._find_config_file(str(tmp_path)) is None + + assert len(caplog.record_tuples) == 1 + ((mod, level, msg),) = caplog.record_tuples + assert (mod, level) == ("flake8.options.config", 30) + assert msg.startswith("ignoring unparseable config ") + + +@pytest.mark.parametrize("cfg_name", ("setup.cfg", "tox.ini", ".flake8")) +def test_find_config_file_exists_at_path(tmp_path, cfg_name): + expected = tmp_path.joinpath(cfg_name) + expected.write_text("[flake8]") + + assert config._find_config_file(str(tmp_path)) == str(expected) + + +@pytest.mark.parametrize("section", ("flake8", "flake8:local-plugins")) +def test_find_config_either_section(tmp_path, section): + expected = tmp_path.joinpath("setup.cfg") + expected.write_text(f"[{section}]") + + assert config._find_config_file(str(tmp_path)) == str(expected) + + +def test_find_config_searches_upwards(tmp_path): + subdir = tmp_path.joinpath("d") + subdir.mkdir() + + expected = tmp_path.joinpath("setup.cfg") + expected.write_text("[flake8]") + + assert config._find_config_file(str(subdir)) == str(expected) + + +def test_load_config_config_specified_skips_discovery(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + custom_cfg = tmpdir.join("custom.cfg") + custom_cfg.write("[flake8]\nindent-size=8\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(str(custom_cfg), [], isolated=False) + + assert cfg.get("flake8", "indent-size") == "8" + assert cfg_dir == str(tmpdir) + + +def test_load_config_no_config_file_does_discovery(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + + assert cfg.get("flake8", "indent-size") == "2" + assert cfg_dir == str(tmpdir) + + +def test_load_config_no_config_found_sets_cfg_dir_to_pwd(tmpdir): + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + + assert cfg.sections() == [] + assert cfg_dir == str(tmpdir) + + +def test_load_config_isolated_ignores_configuration(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=True) + + assert cfg.sections() == [] + assert cfg_dir == str(tmpdir) + + +def test_load_config_append_config(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + other = tmpdir.join("other.cfg") + other.write("[flake8]\nindent-size=8\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [str(other)], isolated=False) + + assert cfg.get("flake8", "indent-size") == "8" + assert cfg_dir == str(tmpdir) + + +@pytest.fixture +def opt_manager(): + ret = OptionManager(prog="flake8", version="123") + register_default_options(ret) + return ret + + +def test_parse_config_no_values(tmp_path, opt_manager): + cfg = configparser.RawConfigParser() + ret = config.parse_config(opt_manager, cfg, tmp_path) + assert ret == {} + + +def test_parse_config_typed_values(tmp_path, opt_manager): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "indent_size", "2") + cfg.set("flake8", "hang_closing", "true") + # test normalizing dashed-options + cfg.set("flake8", "extend-exclude", "d/1,d/2") + + ret = config.parse_config(opt_manager, cfg, str(tmp_path)) + assert ret == { + "indent_size": 2, + "hang_closing": True, + "extend_exclude": [ + str(tmp_path.joinpath("d/1")), + str(tmp_path.joinpath("d/2")), + ], + } + + +def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "wat", "wat") + + ret = config.parse_config(opt_manager, cfg, str(tmp_path)) + assert ret == {} + + assert caplog.record_tuples == [ + ( + "flake8.options.config", + 10, + 'Option "wat" is not registered. Ignoring.', + ) + ] From a30dd75b0a40b5b22cf237da016f06899f2f55e2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 22 Nov 2021 19:56:59 -0500 Subject: [PATCH 034/257] this module is fully typed now --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 372bbb4..bb8f6c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -115,8 +115,6 @@ disallow_untyped_defs = false disallow_untyped_defs = false [mypy-flake8.main.debug] disallow_untyped_defs = false -[mypy-flake8.options.config] -disallow_untyped_defs = false [mypy-flake8.plugins.manager] disallow_untyped_defs = false From 77a054688ba9f2e4e1f925dd89b1279504bddeac Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 25 Nov 2021 15:45:01 -0500 Subject: [PATCH 035/257] refactor run_checks to not take an Optional list of filenames --- src/flake8/api/legacy.py | 3 ++- src/flake8/checker.py | 9 +++------ src/flake8/main/application.py | 11 ++++------- tests/integration/test_checker.py | 2 +- tests/unit/test_checker_manager.py | 12 ++++++------ tests/unit/test_legacy_api.py | 3 ++- 6 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index ed54770..0d9875f 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -103,7 +103,8 @@ class StyleGuide: :rtype: flake8.api.legacy.Report """ - self._application.run_checks(paths) + self._application.options.filenames = paths + self._application.run_checks() self._application.report_errors() return Report(self._application) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 059d05c..d093f39 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -56,21 +56,18 @@ class Manager: together and make our output deterministic. """ - def __init__(self, style_guide, arguments, checker_plugins): + def __init__(self, style_guide, checker_plugins): """Initialize our Manager instance. :param style_guide: The instantiated style guide for this instance of Flake8. :type style_guide: flake8.style_guide.StyleGuide - :param list arguments: - The extra arguments parsed from the CLI (if any) :param checker_plugins: The plugins representing checks parsed from entry-points. :type checker_plugins: flake8.plugins.manager.Checkers """ - self.arguments = arguments self.style_guide = style_guide self.options = style_guide.options self.checks = checker_plugins @@ -112,7 +109,7 @@ class Manager: ) return 0 - if utils.is_using_stdin(self.arguments): + if utils.is_using_stdin(self.options.filenames): LOG.warning( "The --jobs option is not compatible with supplying " "input using - . Ignoring --jobs arguments." @@ -159,7 +156,7 @@ class Manager: def make_checkers(self, paths: Optional[List[str]] = None) -> None: """Create checkers for each file.""" if paths is None: - paths = self.arguments + paths = self.options.filenames checks = self.checks.to_dictionary() self._all_checkers = [ diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 6825f91..4a060f9 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -246,28 +246,25 @@ class Application: def make_file_checker_manager(self) -> None: """Initialize our FileChecker Manager.""" - assert self.options is not None self.file_checker_manager = checker.Manager( style_guide=self.guide, - arguments=self.options.filenames, checker_plugins=self.check_plugins, ) - def run_checks(self, files: Optional[List[str]] = None) -> None: + def run_checks(self) -> None: """Run the actual checks with the FileChecker Manager. This method encapsulates the logic to make a :class:`~flake8.checker.Manger` instance run the checks it is managing. - - :param list files: - List of filenames to process """ assert self.file_checker_manager is not None if self.running_against_diff: - files = sorted(self.parsed_diff) + files: Optional[List[str]] = sorted(self.parsed_diff) if not files: return + else: + files = None self.file_checker_manager.start(files) try: diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 7e0b975..29db6d3 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -293,7 +293,7 @@ def test_report_order(results, expected_order): # Create a placeholder manager without arguments or plugins # Just add one custom file checker which just provides the results - manager = checker.Manager(style_guide, [], []) + manager = checker.Manager(style_guide, []) manager.checkers = manager._all_checkers = [file_checker] # _handle_results is the first place which gets the sorted result diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 6594453..09a0f65 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -22,7 +22,7 @@ def style_guide_mock(): def _parallel_checker_manager(): """Call Manager.run() and return the number of calls to `run_serial`.""" style_guide = style_guide_mock() - manager = checker.Manager(style_guide, [], []) + manager = checker.Manager(style_guide, []) # multiple checkers is needed for parallel mode manager.checkers = [mock.Mock(), mock.Mock()] return manager @@ -54,7 +54,7 @@ def test_oserrors_are_reraised(_): def test_multiprocessing_is_disabled(_): """Verify not being able to import multiprocessing forces jobs to 0.""" style_guide = style_guide_mock() - manager = checker.Manager(style_guide, [], []) + manager = checker.Manager(style_guide, []) assert manager.jobs == 0 @@ -68,7 +68,7 @@ def test_multiprocessing_cpu_count_not_implemented(): "cpu_count", side_effect=NotImplementedError, ): - manager = checker.Manager(style_guide, [], []) + manager = checker.Manager(style_guide, []) assert manager.jobs == 0 @@ -76,14 +76,14 @@ def test_multiprocessing_cpu_count_not_implemented(): def test_make_checkers(_): """Verify that we create a list of FileChecker instances.""" style_guide = style_guide_mock() - files = ["file1", "file2"] + style_guide.options.filenames = ["file1", "file2"] checkplugins = mock.Mock() checkplugins.to_dictionary.return_value = { "ast_plugins": [], "logical_line_plugins": [], "physical_line_plugins": [], } - manager = checker.Manager(style_guide, files, checkplugins) + manager = checker.Manager(style_guide, checkplugins) with mock.patch("flake8.utils.fnmatch", return_value=True): with mock.patch("flake8.processor.FileProcessor"): @@ -91,5 +91,5 @@ def test_make_checkers(_): assert manager._all_checkers for file_checker in manager._all_checkers: - assert file_checker.filename in files + assert file_checker.filename in style_guide.options.filenames assert not manager.checkers # the files don't exist diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 671b21a..1033f5e 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -68,7 +68,8 @@ def test_styleguide_check_files(): style_guide = api.StyleGuide(app) report = style_guide.check_files(paths) - app.run_checks.assert_called_once_with(paths) + assert app.options.filenames == paths + app.run_checks.assert_called_once_with() app.report_errors.assert_called_once_with() assert isinstance(report, api.Report) From a679ab4fb1f4efe5bea92ede70ad2ecb74e3db89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 25 Nov 2021 15:54:33 -0500 Subject: [PATCH 036/257] use self.options.diff directly --- src/flake8/main/application.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 4a060f9..aa2dd14 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -92,8 +92,6 @@ class Application: #: with a non-zero status code self.catastrophic_failure = False - #: Whether the program is processing a diff or not - self.running_against_diff = False #: The parsed diff information self.parsed_diff: Dict[str, Set[int]] = {} @@ -186,8 +184,7 @@ class Application: argv, ) - self.running_against_diff = self.options.diff - if self.running_against_diff: + if self.options.diff: LOG.warning( "the --diff option is deprecated and will be removed in a " "future version." @@ -241,7 +238,7 @@ class Application: self.options, self.formatter ) - if self.running_against_diff: + if self.options.diff: self.guide.add_diff_ranges(self.parsed_diff) def make_file_checker_manager(self) -> None: @@ -258,8 +255,9 @@ class Application: :class:`~flake8.checker.Manger` instance run the checks it is managing. """ + assert self.options is not None assert self.file_checker_manager is not None - if self.running_against_diff: + if self.options.diff: files: Optional[List[str]] = sorted(self.parsed_diff) if not files: return From 878d2a772a26e37f19e869ae1da29aeac53a9850 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:19:46 +0000 Subject: [PATCH 037/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.11b1 → 21.12b0](https://github.com/psf/black/compare/21.11b1...21.12b0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cd8601..16052ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/psf/black - rev: 21.11b1 + rev: 21.12b0 hooks: - id: black args: [--line-length=79] From fa8ac82ee279d85718a67c27673409b31e7f6a0c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Dec 2021 12:25:24 -0800 Subject: [PATCH 038/257] add integration test for off_by_default plugin --- tests/integration/test_plugins.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index dd7b9b7..7d28aff 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -79,6 +79,10 @@ class AlwaysErrors: yield 1, 0, "ABC123 error", type(self) +class AlwaysErrorsDisabled(AlwaysErrors): + off_by_default = True + + def test_plugin_gets_enabled_by_default(tmp_path, capsys): cfg_s = f"""\ [flake8:local-plugins] @@ -95,3 +99,27 @@ extension = out, err = capsys.readouterr() assert out == f"{t_py}:1:1: ABC123 error\n" assert err == "" + + +def test_plugin_off_by_default(tmp_path, capsys): + cfg_s = f"""\ +[flake8:local-plugins] +extension = + ABC = {AlwaysErrorsDisabled.__module__}:{AlwaysErrorsDisabled.__name__} +""" + cfg = tmp_path.joinpath("tox.ini") + cfg.write_text(cfg_s) + + t_py = tmp_path.joinpath("t.py") + t_py.touch() + + cmd = (str(t_py), "--config", str(cfg)) + + assert main(cmd) == 0 + out, err = capsys.readouterr() + assert out == err == "" + + assert main((*cmd, "--enable-extension=ABC")) == 1 + out, err = capsys.readouterr() + assert out == f"{t_py}:1:1: ABC123 error\n" + assert err == "" From 1f76cce31b507853be9978e2b6fca1bfc904b880 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Dec 2021 13:03:26 -0800 Subject: [PATCH 039/257] remove stray noqa --- src/flake8/options/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index a125372..164712e 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -330,7 +330,7 @@ class OptionManager: version: str, usage: str = "%(prog)s [options] file file ...", parents: Optional[List[argparse.ArgumentParser]] = None, - ) -> None: # noqa: E501 + ) -> None: """Initialize an instance of an OptionManager. :param str prog: From 52fb51810467b6c4818b47fdd5a2ab14b53d84bb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Dec 2021 13:47:33 -0800 Subject: [PATCH 040/257] eliminate --bug-report double-parse quirk with store_true --- .pre-commit-config.yaml | 4 +++ src/flake8/main/application.py | 7 ++++++ src/flake8/main/debug.py | 37 ---------------------------- src/flake8/main/options.py | 7 +----- tests/unit/test_debug.py | 45 ---------------------------------- 5 files changed, 12 insertions(+), 88 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16052ba..b6fa672 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,10 @@ repos: hooks: - id: black args: [--line-length=79] +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 - repo: https://github.com/asottile/pyupgrade rev: v2.29.1 hooks: diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index aa2dd14..77f25d4 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -1,6 +1,7 @@ """Module containing the application logic for Flake8.""" import argparse import configparser +import json import logging import sys import time @@ -19,6 +20,7 @@ from flake8 import defaults from flake8 import exceptions from flake8 import style_guide from flake8 import utils +from flake8.main import debug from flake8.main import options from flake8.options import aggregator from flake8.options import config @@ -184,6 +186,11 @@ class Application: argv, ) + if self.options.bug_report: + info = debug.information(self.option_manager) + print(json.dumps(info, indent=2, sort_keys=True)) + raise SystemExit(0) + if self.options.diff: LOG.warning( "the --diff option is deprecated and will be removed in a " diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index 9b3e42b..cca6857 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -1,36 +1,5 @@ """Module containing the logic for our debugging logic.""" -import argparse -import json import platform -from typing import Dict -from typing import List - - -class DebugAction(argparse.Action): - """argparse action to print debug information.""" - - def __init__(self, *args, option_manager, **kwargs): - """Initialize the action. - - This takes an extra `option_manager` keyword argument which will be - used to delay response. - """ - self._option_manager = option_manager - super().__init__(*args, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - """Perform the argparse action for printing debug information.""" - # NOTE(sigmavirus24): Flake8 parses options twice. The first time, we - # will not have any registered plugins. We can skip this one and only - # take action on the second time we're called. - if not self._option_manager.registered_plugins: - return - print( - json.dumps( - information(self._option_manager), indent=2, sort_keys=True - ) - ) - raise SystemExit(0) def information(option_manager): @@ -38,7 +7,6 @@ def information(option_manager): return { "version": option_manager.version, "plugins": plugins_from(option_manager), - "dependencies": dependencies(), "platform": { "python_implementation": platform.python_implementation(), "python_version": platform.python_version(), @@ -57,8 +25,3 @@ def plugins_from(option_manager): } for plugin in sorted(option_manager.registered_plugins) ] - - -def dependencies() -> List[Dict[str, str]]: - """Generate the list of dependencies we care about.""" - return [] diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 3abc043..a73a9ee 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -1,9 +1,7 @@ """Contains the logic for all of the default options for Flake8.""" import argparse -import functools from flake8 import defaults -from flake8.main import debug from flake8.options.manager import OptionManager @@ -379,9 +377,6 @@ def register_default_options(option_manager: OptionManager) -> None: add_option( "--bug-report", - action=functools.partial( - debug.DebugAction, option_manager=option_manager - ), - nargs=0, + action="store_true", help="Print information necessary when preparing a bug report", ) diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index 2da4bf8..e686c5c 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -7,11 +7,6 @@ from flake8.main import debug from flake8.options import manager -def test_dependencies(): - """Verify that we format our dependencies appropriately.""" - assert [] == debug.dependencies() - - @pytest.mark.parametrize( "plugins, expected", [ @@ -75,7 +70,6 @@ def test_information(system, pyversion, pyimpl): {"plugin": "mccabe", "version": "0.5.9", "is_local": False}, {"plugin": "pycodestyle", "version": "2.0.0", "is_local": False}, ], - "dependencies": [], "platform": { "python_implementation": "CPython", "python_version": "3.5.3", @@ -93,42 +87,3 @@ def test_information(system, pyversion, pyimpl): pyimpl.assert_called_once_with() pyversion.assert_called_once_with() system.assert_called_once_with() - - -@mock.patch("flake8.main.debug.print") -@mock.patch("flake8.main.debug.information", return_value={}) -@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.""" - option_manager = mock.Mock(registered_plugins=set()) - action = debug.DebugAction( - "--bug-report", - dest="bug_report", - option_manager=option_manager, - ) - assert action(None, None, None, None) is None - assert dumps.called is False - assert information.called is False - assert print_mock.called is False - - -@mock.patch("flake8.main.debug.print") -@mock.patch("flake8.main.debug.information", return_value={}) -@mock.patch("json.dumps", return_value="{}") -def test_print_information(dumps, information, print_mock): - """Verify we print and exit only when we have plugins.""" - plugins = [ - manager.PluginVersion("pycodestyle", "2.0.0", False), - manager.PluginVersion("mccabe", "0.5.9", False), - ] - option_manager = mock.Mock(registered_plugins=set(plugins)) - action = debug.DebugAction( - "--bug-report", - dest="bug_report", - option_manager=option_manager, - ) - with pytest.raises(SystemExit): - action(None, None, None, None) - print_mock.assert_called_once_with("{}") - dumps.assert_called_once_with({}, indent=2, sort_keys=True) - information.assert_called_once_with(option_manager) From 3fa044ca4b0862126e7718698aa72d1cc1ca890d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Dec 2021 15:28:58 -0800 Subject: [PATCH 041/257] return an argparser instead of side-effects --- src/flake8/main/application.py | 3 +-- src/flake8/main/options.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 77f25d4..9d4ff27 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -55,8 +55,7 @@ class Application: self.version = version #: The prelimary argument parser for handling options required for #: obtaining and parsing the configuration file. - self.prelim_arg_parser = argparse.ArgumentParser(add_help=False) - options.register_preliminary_options(self.prelim_arg_parser) + self.prelim_arg_parser = options.stage1_arg_parser() #: The instance of :class:`flake8.options.manager.OptionManager` used #: to parse and handle the options and arguments passed by the user self.option_manager = manager.OptionManager( diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index a73a9ee..9c23d64 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -5,7 +5,7 @@ from flake8 import defaults from flake8.options.manager import OptionManager -def register_preliminary_options(parser: argparse.ArgumentParser) -> None: +def stage1_arg_parser() -> argparse.ArgumentParser: """Register the preliminary options on our OptionManager. The preliminary options include: @@ -16,9 +16,9 @@ def register_preliminary_options(parser: argparse.ArgumentParser) -> None: - ``--config`` - ``--isolated`` """ - add_argument = parser.add_argument + parser = argparse.ArgumentParser(add_help=False) - add_argument( + parser.add_argument( "-v", "--verbose", default=0, @@ -28,13 +28,13 @@ def register_preliminary_options(parser: argparse.ArgumentParser) -> None: "time it is repeated.", ) - add_argument( + parser.add_argument( "--output-file", default=None, help="Redirect report to a file." ) # Config file options - add_argument( + parser.add_argument( "--append-config", action="append", default=[], @@ -44,7 +44,7 @@ def register_preliminary_options(parser: argparse.ArgumentParser) -> None: "provide the same option.", ) - add_argument( + parser.add_argument( "--config", default=None, help="Path to the config file that will be the authoritative config " @@ -52,13 +52,15 @@ def register_preliminary_options(parser: argparse.ArgumentParser) -> None: "configuration files.", ) - add_argument( + parser.add_argument( "--isolated", default=False, action="store_true", help="Ignore all configuration files.", ) + return parser + class JobsArgument: """Type callback for the --jobs argument.""" From 1e4743d490040c320b0c453edca15f90c7bf1bc7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Dec 2021 15:36:20 -0800 Subject: [PATCH 042/257] use plugin_name= instead of dicts in exceptions --- src/flake8/checker.py | 4 ++-- src/flake8/exceptions.py | 17 ++++++++--------- tests/unit/test_exceptions.py | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index d093f39..63fa439 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -353,7 +353,7 @@ class FileChecker: except AttributeError as ae: LOG.error("Plugin requested unknown parameters.") raise exceptions.PluginRequestedUnknownParameters( - plugin=plugin, exception=ae + plugin_name=plugin["plugin_name"], exception=ae ) try: return plugin["plugin"](**arguments) @@ -364,7 +364,7 @@ class FileChecker: exc_info=True, ) raise exceptions.PluginExecutionFailed( - plugin=plugin, exception=all_exc + plugin_name=plugin["plugin_name"], exception=all_exc ) @staticmethod diff --git a/src/flake8/exceptions.py b/src/flake8/exceptions.py index 45db94d..e2dfd77 100644 --- a/src/flake8/exceptions.py +++ b/src/flake8/exceptions.py @@ -1,5 +1,4 @@ """Exception classes for all of Flake8.""" -from typing import Dict class Flake8Exception(Exception): @@ -38,16 +37,16 @@ class PluginRequestedUnknownParameters(Flake8Exception): FORMAT = '"%(name)s" requested unknown parameters causing %(exc)s' - def __init__(self, plugin: Dict[str, str], exception: Exception) -> None: + def __init__(self, plugin_name: str, exception: Exception) -> None: """Pop certain keyword arguments for initialization.""" - self.plugin = plugin + self.plugin_name = plugin_name self.original_exception = exception - super().__init__(plugin, exception) + super().__init__(plugin_name, exception) def __str__(self) -> str: """Format our exception message.""" return self.FORMAT % { - "name": self.plugin["plugin_name"], + "name": self.plugin_name, "exc": self.original_exception, } @@ -57,15 +56,15 @@ class PluginExecutionFailed(Flake8Exception): FORMAT = '"%(name)s" failed during execution due to "%(exc)s"' - def __init__(self, plugin: Dict[str, str], exception: Exception) -> None: + def __init__(self, plugin_name: str, exception: Exception) -> None: """Utilize keyword arguments for message generation.""" - self.plugin = plugin + self.plugin_name = plugin_name self.original_exception = exception - super().__init__(plugin, exception) + super().__init__(plugin_name, exception) def __str__(self) -> str: """Format our exception message.""" return self.FORMAT % { - "name": self.plugin["plugin_name"], + "name": self.plugin_name, "exc": self.original_exception, } diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 6be1ebd..06c5179 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -14,11 +14,11 @@ from flake8 import exceptions exception=ValueError("boom!"), ), exceptions.PluginRequestedUnknownParameters( - plugin={"plugin_name": "plugin_name"}, + plugin_name="plugin_name", exception=ValueError("boom!"), ), exceptions.PluginExecutionFailed( - plugin={"plugin_name": "plugin_name"}, + plugin_name="plugin_name", exception=ValueError("boom!"), ), ), From f98d52a398cd2ff5cad270fdee9e37b62444550a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Dec 2021 15:49:17 -0500 Subject: [PATCH 043/257] have OptionManager take plugin versions directly --- setup.cfg | 2 - src/flake8/main/application.py | 31 +++++---- src/flake8/main/debug.py | 24 ++++--- src/flake8/options/manager.py | 92 +++++--------------------- src/flake8/plugins/manager.py | 6 -- tests/integration/test_aggregator.py | 3 +- tests/unit/test_debug.py | 69 +++++-------------- tests/unit/test_option_manager.py | 99 ++-------------------------- tests/unit/test_options_config.py | 2 +- 9 files changed, 73 insertions(+), 255 deletions(-) diff --git a/setup.cfg b/setup.cfg index bb8f6c3..47f8feb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -113,8 +113,6 @@ disallow_untyped_defs = false disallow_untyped_defs = false [mypy-flake8.main.application] disallow_untyped_defs = false -[mypy-flake8.main.debug] -disallow_untyped_defs = false [mypy-flake8.plugins.manager] disallow_untyped_defs = false diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 9d4ff27..bf43826 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -37,7 +37,7 @@ LOG = logging.getLogger(__name__) class Application: """Abstract our application into a class.""" - def __init__(self, program="flake8", version=flake8.__version__): + def __init__(self) -> None: """Initialize our application. :param str program: @@ -49,21 +49,12 @@ class Application: self.start_time = time.time() #: The timestamp when the Application finished reported errors. self.end_time: Optional[float] = None - #: The name of the program being run - self.program = program - #: The version of the program being run - self.version = version #: The prelimary argument parser for handling options required for #: obtaining and parsing the configuration file. self.prelim_arg_parser = options.stage1_arg_parser() #: The instance of :class:`flake8.options.manager.OptionManager` used #: to parse and handle the options and arguments passed by the user - self.option_manager = manager.OptionManager( - prog="flake8", - version=flake8.__version__, - parents=[self.prelim_arg_parser], - ) - options.register_default_options(self.option_manager) + self.option_manager: Optional[manager.OptionManager] = None #: The instance of :class:`flake8.plugins.manager.Checkers` self.check_plugins: Optional[plugin_manager.Checkers] = None @@ -166,9 +157,19 @@ class Application: def register_plugin_options(self) -> None: """Register options provided by plugins to our option manager.""" assert self.check_plugins is not None - self.check_plugins.register_options(self.option_manager) - self.check_plugins.register_plugin_versions(self.option_manager) assert self.formatting_plugins is not None + + versions = sorted(set(self.check_plugins.manager.versions())) + self.option_manager = manager.OptionManager( + version=flake8.__version__, + plugin_versions=", ".join( + f"{name}: {version}" for name, version in versions + ), + parents=[self.prelim_arg_parser], + ) + options.register_default_options(self.option_manager) + + self.check_plugins.register_options(self.option_manager) self.formatting_plugins.register_options(self.option_manager) def parse_configuration_and_cli( @@ -178,6 +179,7 @@ class Application: argv: List[str], ) -> None: """Parse configuration files and the CLI options.""" + assert self.option_manager is not None self.options = aggregator.aggregate_options( self.option_manager, cfg, @@ -186,7 +188,8 @@ class Application: ) if self.options.bug_report: - info = debug.information(self.option_manager) + assert self.check_plugins is not None + info = debug.information(flake8.__version__, self.check_plugins) print(json.dumps(info, indent=2, sort_keys=True)) raise SystemExit(0) diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index cca6857..4cd9024 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -1,12 +1,20 @@ """Module containing the logic for our debugging logic.""" import platform +from typing import Any +from typing import Dict +from typing import List + +from flake8.plugins.manager import PluginTypeManager -def information(option_manager): +def information( + version: str, + plugins: PluginTypeManager, +) -> Dict[str, Any]: """Generate the information to be printed for the bug report.""" return { - "version": option_manager.version, - "plugins": plugins_from(option_manager), + "version": version, + "plugins": plugins_from(plugins), "platform": { "python_implementation": platform.python_implementation(), "python_version": platform.python_version(), @@ -15,13 +23,9 @@ def information(option_manager): } -def plugins_from(option_manager): +def plugins_from(plugins: PluginTypeManager) -> List[Dict[str, str]]: """Generate the list of plugins installed.""" return [ - { - "plugin": plugin.name, - "version": plugin.version, - "is_local": plugin.local, - } - for plugin in sorted(option_manager.registered_plugins) + {"plugin": name, "version": version} + for name, version in sorted(set(plugins.manager.versions())) ] diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 164712e..5d48ba9 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -1,13 +1,11 @@ """Option handling and Option management logic.""" import argparse -import collections import contextlib import enum import functools import logging from typing import Any from typing import Callable -from typing import cast from typing import Dict from typing import Generator from typing import List @@ -316,20 +314,15 @@ class Option: return self.option_args, self.filtered_option_kwargs -PluginVersion = collections.namedtuple( - "PluginVersion", ["name", "version", "local"] -) - - class OptionManager: """Manage Options and OptionParser while adding post-processing.""" def __init__( self, - prog: str, + *, version: str, - usage: str = "%(prog)s [options] file file ...", - parents: Optional[List[argparse.ArgumentParser]] = None, + plugin_versions: str, + parents: List[argparse.ArgumentParser], ) -> None: """Initialize an instance of an OptionManager. @@ -343,28 +336,28 @@ class OptionManager: A list of ArgumentParser objects whose arguments should also be included. """ - if parents is None: - parents = [] - - self.parser: argparse.ArgumentParser = argparse.ArgumentParser( - prog=prog, usage=usage, parents=parents + self.parser = argparse.ArgumentParser( + prog="flake8", + usage="%(prog)s [options] file file ...", + parents=parents, + epilog=f"Installed plugins: {plugin_versions}", ) - self._current_group: Optional[argparse._ArgumentGroup] = None - self.version_action = cast( - "argparse._VersionAction", - self.parser.add_argument( - "--version", action="version", version=version + self.parser.add_argument( + "--version", + action="version", + version=( + f"{version} ({plugin_versions}) " + f"{utils.get_python_version()}" ), ) self.parser.add_argument("filenames", nargs="*", metavar="filename") self.config_options_dict: Dict[str, Option] = {} self.options: List[Option] = [] - self.program_name = prog - self.version = version - self.registered_plugins: Set[PluginVersion] = set() self.extended_default_ignore: Set[str] = set() self.extended_default_select: Set[str] = set() + self._current_group: Optional[argparse._ArgumentGroup] = None + @contextlib.contextmanager def group(self, name: str) -> Generator[None, None, None]: """Attach options to an argparse group during this context.""" @@ -395,7 +388,7 @@ class OptionManager: self.options.append(option) if option.parse_from_config: name = option.config_name - assert name is not None # nosec (for mypy) + assert name is not None self.config_options_dict[name] = option self.config_options_dict[name.replace("_", "-")] = option LOG.debug('Registered option "%s".', option) @@ -438,63 +431,12 @@ class OptionManager: LOG.debug("Extending default select list with %r", error_codes) self.extended_default_select.update(error_codes) - def generate_versions( - self, format_str: str = "%(name)s: %(version)s", join_on: str = ", " - ) -> str: - """Generate a comma-separated list of versions of plugins.""" - return join_on.join( - format_str % plugin._asdict() - for plugin in sorted(self.registered_plugins) - ) - - def update_version_string(self) -> None: - """Update the flake8 version string.""" - self.version_action.version = "{} ({}) {}".format( - self.version, self.generate_versions(), utils.get_python_version() - ) - - def generate_epilog(self) -> None: - """Create an epilog with the version and name of each of plugin.""" - plugin_version_format = "%(name)s: %(version)s" - self.parser.epilog = "Installed plugins: " + self.generate_versions( - plugin_version_format - ) - def parse_args( self, args: Optional[Sequence[str]] = None, values: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: """Proxy to calling the OptionParser's parse_args method.""" - self.generate_epilog() - self.update_version_string() if values: self.parser.set_defaults(**vars(values)) return self.parser.parse_args(args) - - def parse_known_args( - self, args: Optional[List[str]] = None - ) -> Tuple[argparse.Namespace, List[str]]: - """Parse only the known arguments from the argument values. - - Replicate a little argparse behaviour while we're still on - optparse. - """ - self.generate_epilog() - self.update_version_string() - return self.parser.parse_known_args(args) - - def register_plugin( - self, name: str, version: str, local: bool = False - ) -> None: - """Register a plugin relying on the OptionManager. - - :param str name: - The name of the checker itself. This will be the ``name`` - attribute of the class or function loaded from the entry-point. - :param str version: - The version of the checker that we're using. - :param bool local: - Whether the plugin is local to the project/repository or not. - """ - self.registered_plugins.add(PluginVersion(name, version, local)) diff --git a/src/flake8/plugins/manager.py b/src/flake8/plugins/manager.py index d2b9187..fe9b038 100644 --- a/src/flake8/plugins/manager.py +++ b/src/flake8/plugins/manager.py @@ -427,12 +427,6 @@ class PluginTypeManager: # Do not set plugins_loaded if we run into an exception self.plugins_loaded = True - def register_plugin_versions(self, optmanager): - """Register the plugins and their versions with the OptionManager.""" - self.load_plugins() - for (plugin_name, version) in self.manager.versions(): - optmanager.register_plugin(name=plugin_name, version=version) - def register_options(self, optmanager): """Register all of the checkers' options to the OptionManager.""" self.load_plugins() diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py index cdc7281..de9e6fb 100644 --- a/tests/integration/test_aggregator.py +++ b/tests/integration/test_aggregator.py @@ -13,8 +13,9 @@ from flake8.options import manager def optmanager(): """Create a new OptionManager.""" option_manager = manager.OptionManager( - prog="flake8", version="3.0.0", + plugin_versions="", + parents=[], ) options.register_default_options(option_manager) return option_manager diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index e686c5c..28ab48e 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -4,58 +4,24 @@ from unittest import mock import pytest from flake8.main import debug -from flake8.options import manager @pytest.mark.parametrize( - "plugins, expected", - [ + ("versions", "expected"), + ( ([], []), ( - [manager.PluginVersion("pycodestyle", "2.0.0", False)], + [("p1", "1"), ("p2", "2"), ("p1", "1")], [ - { - "plugin": "pycodestyle", - "version": "2.0.0", - "is_local": False, - } + {"plugin": "p1", "version": "1"}, + {"plugin": "p2", "version": "2"}, ], ), - ( - [ - manager.PluginVersion("pycodestyle", "2.0.0", False), - manager.PluginVersion("mccabe", "0.5.9", False), - ], - [ - {"plugin": "mccabe", "version": "0.5.9", "is_local": False}, - { - "plugin": "pycodestyle", - "version": "2.0.0", - "is_local": False, - }, - ], - ), - ( - [ - manager.PluginVersion("pycodestyle", "2.0.0", False), - manager.PluginVersion("my-local", "0.0.1", True), - manager.PluginVersion("mccabe", "0.5.9", False), - ], - [ - {"plugin": "mccabe", "version": "0.5.9", "is_local": False}, - {"plugin": "my-local", "version": "0.0.1", "is_local": True}, - { - "plugin": "pycodestyle", - "version": "2.0.0", - "is_local": False, - }, - ], - ), - ], + ), ) -def test_plugins_from(plugins, expected): +def test_plugins_from(versions, expected): """Test that we format plugins appropriately.""" - option_manager = mock.Mock(registered_plugins=set(plugins)) + option_manager = mock.Mock(**{"manager.versions.return_value": versions}) assert expected == debug.plugins_from(option_manager) @@ -67,8 +33,8 @@ def test_information(system, pyversion, pyimpl): expected = { "version": "3.1.0", "plugins": [ - {"plugin": "mccabe", "version": "0.5.9", "is_local": False}, - {"plugin": "pycodestyle", "version": "2.0.0", "is_local": False}, + {"plugin": "mccabe", "version": "0.5.9"}, + {"plugin": "pycodestyle", "version": "2.0.0"}, ], "platform": { "python_implementation": "CPython", @@ -76,14 +42,15 @@ def test_information(system, pyversion, pyimpl): "system": "Linux", }, } - option_manager = mock.Mock( - registered_plugins={ - manager.PluginVersion("pycodestyle", "2.0.0", False), - manager.PluginVersion("mccabe", "0.5.9", False), - }, - version="3.1.0", + plugins = mock.Mock( + **{ + "manager.versions.return_value": [ + ("pycodestyle", "2.0.0"), + ("mccabe", "0.5.9"), + ] + } ) - assert expected == debug.information(option_manager) + assert expected == debug.information("3.1.0", plugins) pyimpl.assert_called_once_with() pyversion.assert_called_once_with() system.assert_called_once_with() diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index 0dee442..c6006d3 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -5,7 +5,6 @@ from unittest import mock import pytest -from flake8 import utils from flake8.main.options import JobsArgument from flake8.options import manager @@ -15,7 +14,9 @@ TEST_VERSION = "3.0.0b1" @pytest.fixture def optmanager(): """Generate a simple OptionManager with default test arguments.""" - return manager.OptionManager(prog="flake8", version=TEST_VERSION) + return manager.OptionManager( + version=TEST_VERSION, plugin_versions="", parents=[] + ) def test_option_manager_creates_option_parser(optmanager): @@ -31,7 +32,7 @@ def test_option_manager_including_parent_options(): # WHEN optmanager = manager.OptionManager( - prog="flake8", version=TEST_VERSION, parents=[parent_parser] + version=TEST_VERSION, plugin_versions="", parents=[parent_parser] ) options = optmanager.parse_args(["--parent", "foo"]) @@ -153,90 +154,6 @@ def test_parse_args_normalize_paths(optmanager): ] -def test_generate_versions(optmanager): - """Verify a comma-separated string is generated of registered plugins.""" - optmanager.registered_plugins = [ - manager.PluginVersion("Testing 100", "0.0.0", False), - manager.PluginVersion("Testing 101", "0.0.0", False), - manager.PluginVersion("Testing 300", "0.0.0", True), - ] - assert ( - optmanager.generate_versions() - == "Testing 100: 0.0.0, Testing 101: 0.0.0, Testing 300: 0.0.0" - ) - - -def test_plugins_are_sorted_in_generate_versions(optmanager): - """Verify we sort before joining strings in generate_versions.""" - optmanager.registered_plugins = [ - manager.PluginVersion("pyflakes", "1.5.0", False), - manager.PluginVersion("mccabe", "0.7.0", False), - manager.PluginVersion("pycodestyle", "2.2.0", False), - manager.PluginVersion("flake8-docstrings", "0.6.1", False), - manager.PluginVersion("flake8-bugbear", "2016.12.1", False), - ] - assert ( - optmanager.generate_versions() == "flake8-bugbear: 2016.12.1, " - "flake8-docstrings: 0.6.1, " - "mccabe: 0.7.0, " - "pycodestyle: 2.2.0, " - "pyflakes: 1.5.0" - ) - - -def test_generate_versions_with_format_string(optmanager): - """Verify a comma-separated string is generated of registered plugins.""" - optmanager.registered_plugins.update( - [ - manager.PluginVersion("Testing", "0.0.0", False), - manager.PluginVersion("Testing", "0.0.0", False), - manager.PluginVersion("Testing", "0.0.0", False), - ] - ) - assert optmanager.generate_versions() == "Testing: 0.0.0" - - -def test_update_version_string(optmanager): - """Verify we update the version string idempotently.""" - assert optmanager.version == TEST_VERSION - assert optmanager.version_action.version == TEST_VERSION - - optmanager.registered_plugins = [ - manager.PluginVersion("Testing 100", "0.0.0", False), - manager.PluginVersion("Testing 101", "0.0.0", False), - manager.PluginVersion("Testing 300", "0.0.0", False), - ] - - optmanager.update_version_string() - - assert optmanager.version == TEST_VERSION - assert ( - optmanager.version_action.version - == TEST_VERSION - + " (Testing 100: 0.0.0, Testing 101: 0.0.0, Testing 300: 0.0.0) " - + utils.get_python_version() - ) - - -def test_generate_epilog(optmanager): - """Verify how we generate the epilog for help text.""" - assert optmanager.parser.epilog is None - - optmanager.registered_plugins = [ - manager.PluginVersion("Testing 100", "0.0.0", False), - manager.PluginVersion("Testing 101", "0.0.0", False), - manager.PluginVersion("Testing 300", "0.0.0", False), - ] - - expected_value = ( - "Installed plugins: Testing 100: 0.0.0, Testing 101: 0.0.0, Testing" - " 300: 0.0.0" - ) - - optmanager.generate_epilog() - assert optmanager.parser.epilog == expected_value - - def test_extend_default_ignore(optmanager): """Verify that we update the extended default ignore list.""" assert optmanager.extended_default_ignore == set() @@ -245,14 +162,6 @@ def test_extend_default_ignore(optmanager): assert optmanager.extended_default_ignore == {"T100", "T101", "T102"} -def test_parse_known_args(optmanager): - """Verify we ignore unknown options.""" - with mock.patch("sys.exit") as sysexit: - optmanager.parse_known_args(["--max-complexity", "5"]) - - assert sysexit.called is False - - def test_optparse_normalize_callback_option_legacy(optmanager): """Test the optparse shim for `callback=`.""" callback_foo = mock.Mock() diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 75bfac5..b288de0 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -119,7 +119,7 @@ def test_load_config_append_config(tmpdir): @pytest.fixture def opt_manager(): - ret = OptionManager(prog="flake8", version="123") + ret = OptionManager(version="123", plugin_versions="", parents=[]) register_default_options(ret) return ret From 19db9086057c4bbcf47d65d07a1b747f39625908 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:41:31 +0000 Subject: [PATCH 044/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.910-1 → v0.920](https://github.com/pre-commit/mirrors-mypy/compare/v0.910-1...v0.920) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b6fa672..f0a22b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.920 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From a7be5e798b06ef24d4c61f57a4ba65437576911e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 24 Dec 2021 16:38:17 -0500 Subject: [PATCH 045/257] fix AttributeError when catatstrophic failure is triggered --- src/flake8/main/application.py | 14 ++++++++------ tests/integration/test_main.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index bf43826..1894881 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -115,11 +115,13 @@ class Application: def exit_code(self) -> int: """Return the program exit code.""" + if self.catastrophic_failure: + return 1 assert self.options is not None if self.options.exit_zero: - return int(self.catastrophic_failure) + return 0 else: - return int((self.result_count > 0) or self.catastrophic_failure) + return int(self.result_count > 0) def find_plugins( self, @@ -391,7 +393,7 @@ class Application: except exceptions.EarlyQuit: self.catastrophic_failure = True print("... stopped while processing files") - - assert self.options is not None - if self.options.count: - print(self.result_count) + else: + assert self.options is not None + if self.options.count: + print(self.result_count) diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 27acab5..b351e9d 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -8,6 +8,7 @@ import pytest from flake8 import utils from flake8.main import cli +from flake8.options import config def test_diff_option(tmpdir, capsys): @@ -375,3 +376,13 @@ def test_output_file(tmpdir, capsys): expected = "t.py:1:1: F401 'os' imported but unused\n" assert tmpdir.join("a/b/f").read() == expected + + +def test_early_keyboard_interrupt_does_not_crash(capsys): + with mock.patch.object( + config, "load_config", side_effect=KeyboardInterrupt + ): + assert cli.main(["does-not-exist"]) == 1 + out, err = capsys.readouterr() + assert out == "... stopped\n" + assert err == "" From d478d92299d4025067e17beb4f5ec565f0b3033e Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Fri, 24 Dec 2021 01:35:27 +0000 Subject: [PATCH 046/257] add failing test for missing config file --- tests/integration/test_main.py | 18 ++++++++++++++++++ tests/unit/test_options_config.py | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index b351e9d..90877d3 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -386,3 +386,21 @@ def test_early_keyboard_interrupt_does_not_crash(capsys): out, err = capsys.readouterr() assert out == "... stopped\n" assert err == "" + + +def test_config_file_not_found(tmpdir, capsys): + """Ensure that an explicitly specified config file which is not found is an + error""" + + expected = """\ +There was a critical error during execution of Flake8: +The specified config file does not exist +""" + + with tmpdir.as_cwd(): + tmpdir.join("t.py").write("print('hello hello world')\n") + assert cli.main(["--config", "missing.cfg", "t.py"]) == 1 + + out, err = capsys.readouterr() + assert out == expected + assert err == "" diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index b288de0..c5d1476 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -2,6 +2,7 @@ import configparser import pytest +from flake8 import exceptions from flake8.main.options import register_default_options from flake8.options import config from flake8.options.manager import OptionManager @@ -164,3 +165,8 @@ def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog): 'Option "wat" is not registered. Ignoring.', ) ] + + +def test_load_config_missing_file_raises_exception(capsys): + with pytest.raises(exceptions.ExecutionError): + config.load_config("foo.cfg", []) From d948169292388116c45febad470162ba5652dd60 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Fri, 24 Dec 2021 01:36:41 +0000 Subject: [PATCH 047/257] add check for a missing specified config file --- src/flake8/options/config.py | 6 +++++- tests/integration/test_main.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index d7519df..7cba936 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -8,6 +8,7 @@ from typing import List from typing import Optional from typing import Tuple +from flake8 import exceptions from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) @@ -60,7 +61,10 @@ def load_config( cfg = configparser.RawConfigParser() if config is not None: - cfg.read(config) + if not cfg.read(config): + raise exceptions.ExecutionError( + f"The specified config file does not exist: {config}" + ) cfg_dir = os.path.dirname(config) else: cfg_dir = pwd diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 90877d3..fe254b7 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -394,7 +394,7 @@ def test_config_file_not_found(tmpdir, capsys): expected = """\ There was a critical error during execution of Flake8: -The specified config file does not exist +The specified config file does not exist: missing.cfg """ with tmpdir.as_cwd(): From 8d3c6ed5912632a1e3db2c4332d105416bb96dcf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Dec 2021 22:23:20 +0000 Subject: [PATCH 048/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0) - [github.com/pre-commit/mirrors-mypy: v0.920 → v0.930](https://github.com/pre-commit/mirrors-mypy/compare/v0.920...v0.930) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0a22b8..4ec1266 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: check-yaml - id: debug-statements @@ -27,7 +27,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.920 + rev: v0.930 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 50d69150c1073ca5e5144a9ef05bfab25ffb3c00 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Dec 2021 13:11:00 -0800 Subject: [PATCH 049/257] rework plugin loading --- docs/source/internal/plugin_handling.rst | 74 +-- docs/source/internal/utils.rst | 17 - .../plugin-development/plugin-parameters.rst | 4 +- .../flake8_example_plugin/off_by_default.py | 3 - .../flake8_example_plugin/on_by_default.py | 3 - pytest.ini | 6 +- setup.cfg | 8 +- src/flake8/checker.py | 76 +-- src/flake8/main/application.py | 112 +--- src/flake8/main/debug.py | 28 +- src/flake8/options/manager.py | 39 +- src/flake8/plugins/finder.py | 256 ++++++++ src/flake8/plugins/manager.py | 530 --------------- src/flake8/plugins/pyflakes.py | 2 - src/flake8/plugins/reporter.py | 41 ++ src/flake8/processor.py | 7 +- src/flake8/utils.py | 39 -- tests/fixtures/config_files/README.rst | 22 - .../config_files/local-plugin-path.ini | 5 - tests/fixtures/config_files/local-plugin.ini | 5 - tests/integration/subdir/aplugin.py | 3 - tests/integration/test_checker.py | 76 +-- tests/integration/test_plugins.py | 90 ++- tests/unit/plugins/__init__.py | 0 tests/unit/plugins/finder_test.py | 610 ++++++++++++++++++ tests/unit/plugins/reporter_test.py | 74 +++ tests/unit/test_application.py | 61 -- tests/unit/test_checker_manager.py | 15 +- tests/unit/test_debug.py | 77 +-- tests/unit/test_file_checker.py | 39 +- tests/unit/test_main_options.py | 17 + tests/unit/test_option_manager.py | 11 - tests/unit/test_plugin.py | 169 ----- tests/unit/test_plugin_manager.py | 57 -- tests/unit/test_plugin_type_manager.py | 177 ----- tests/unit/test_utils.py | 29 - 36 files changed, 1277 insertions(+), 1505 deletions(-) create mode 100644 src/flake8/plugins/finder.py delete mode 100644 src/flake8/plugins/manager.py create mode 100644 src/flake8/plugins/reporter.py delete mode 100644 tests/fixtures/config_files/README.rst delete mode 100644 tests/fixtures/config_files/local-plugin-path.ini delete mode 100644 tests/fixtures/config_files/local-plugin.ini create mode 100644 tests/unit/plugins/__init__.py create mode 100644 tests/unit/plugins/finder_test.py create mode 100644 tests/unit/plugins/reporter_test.py create mode 100644 tests/unit/test_main_options.py delete mode 100644 tests/unit/test_plugin.py delete mode 100644 tests/unit/test_plugin_manager.py delete mode 100644 tests/unit/test_plugin_type_manager.py diff --git a/docs/source/internal/plugin_handling.rst b/docs/source/internal/plugin_handling.rst index faf3996..cdec601 100644 --- a/docs/source/internal/plugin_handling.rst +++ b/docs/source/internal/plugin_handling.rst @@ -11,44 +11,6 @@ new checks. It now supports: - alternative report formatters -To facilitate this, |Flake8| needed a more mature way of managing plugins. -Thus, we developed the |PluginManager| which accepts a namespace and will load -the plugins for that namespace. A |PluginManager| creates and manages many -|Plugin| instances. - -A |Plugin| lazily loads the underlying entry-point provided by setuptools. -The entry-point will be loaded either by calling -:meth:`~flake8.plugins.manager.Plugin.load_plugin` or accessing the ``plugin`` -attribute. We also use this abstraction to retrieve options that the plugin -wishes to register and parse. - -The only public method the |PluginManager| provides is -:meth:`~flake8.plugins.manager.PluginManager.map`. This will accept a function -(or other callable) and call it with each plugin as the first parameter. - -We build atop the |PluginManager| with the |PTM|. It is expected that users of -the |PTM| will subclass it and specify the ``namespace``, e.g., - -.. code-block:: python - - class ExamplePluginType(flake8.plugin.manager.PluginTypeManager): - namespace = 'example-plugins' - -This provides a few extra methods via the |PluginManager|'s ``map`` method. - -Finally, we create two classes of plugins: - -- :class:`~flake8.plugins.manager.Checkers` - -- :class:`~flake8.plugins.manager.ReportFormatters` - -These are used to interact with each of the types of plugins individually. - -.. note:: - - Our inspiration for our plugin handling comes from the author's extensive - experience with ``stevedore``. - Default Plugins --------------- @@ -56,40 +18,26 @@ Finally, |Flake8| has always provided its own plugin shim for Pyflakes. As part of that we carry our own shim in-tree and now store that in :mod:`flake8.plugins.pyflakes`. -|Flake8| also registers plugins for pep8. Each check in pep8 requires -different parameters and it cannot easily be shimmed together like Pyflakes -was. As such, plugins have a concept of a "group". If you look at our -:file:`setup.py` you will see that we register pep8 checks roughly like so: +|Flake8| also registers plugins for pycodestyle. Each check in pycodestyle +requires different parameters and it cannot easily be shimmed together like +Pyflakes was. As such, plugins have a concept of a "group". If you look at our +:file:`setup.py` you will see that we register pycodestyle checks roughly like +so: .. code:: - pep8. = pep8: + pycodestyle. = pycodestyle: We do this to identify that ``>`` is part of a group. This also enables us to special-case how we handle reporting those checks. Instead of -reporting each check in the ``--version`` output, we report ``pep8`` and check -``pep8`` the module for a ``__version__`` attribute. We only report it once -to avoid confusing users. +reporting each check in the ``--version`` output, we only report +``pycodestyle`` once. API Documentation ----------------- -.. autoclass:: flake8.plugins.manager.PluginManager - :members: - :special-members: __init__ +.. autofunction:: flake8.plugins.finder.find_plugins -.. autoclass:: flake8.plugins.manager.Plugin - :members: - :special-members: __init__ +.. autofunction:: flake8.plugins.finder.find_local_plugin_paths -.. autoclass:: flake8.plugins.manager.PluginTypeManager - :members: - -.. autoclass:: flake8.plugins.manager.Checkers - :members: - -.. autoclass:: flake8.plugins.manager.ReportFormatters - -.. |PluginManager| replace:: :class:`~flake8.plugins.manager.PluginManager` -.. |Plugin| replace:: :class:`~flake8.plugins.manager.Plugin` -.. |PTM| replace:: :class:`~flake8.plugins.manager.PluginTypeManager` +.. autofunction:: flake8.plugins.finder.load_plugins diff --git a/docs/source/internal/utils.rst b/docs/source/internal/utils.rst index 34b09f1..c745917 100644 --- a/docs/source/internal/utils.rst +++ b/docs/source/internal/utils.rst @@ -67,23 +67,6 @@ filename matches a single pattern. In our use case, however, we typically have a list of patterns and want to know if the filename matches any of them. This function abstracts that logic away with a little extra logic. -.. autofunction:: flake8.utils.parameters_for - -|Flake8| analyzes the parameters to plugins to determine what input they are -expecting. Plugins may expect one of the following: - -- ``physical_line`` to receive the line as it appears in the file - -- ``logical_line`` to receive the logical line (not as it appears in the file) - -- ``tree`` to receive the abstract syntax tree (AST) for the file - -We also analyze the rest of the parameters to provide more detail to the -plugin. This function will return the parameters in a consistent way across -versions of Python and will handle both classes and functions that are used as -plugins. Further, if the plugin is a class, it will strip the ``self`` -argument so we can check the parameters of the plugin consistently. - .. autofunction:: flake8.utils.parse_unified_diff To handle usage of :option:`flake8 --diff`, |Flake8| needs to be able diff --git a/docs/source/plugin-development/plugin-parameters.rst b/docs/source/plugin-development/plugin-parameters.rst index 6dae857..931c186 100644 --- a/docs/source/plugin-development/plugin-parameters.rst +++ b/docs/source/plugin-development/plugin-parameters.rst @@ -22,8 +22,8 @@ Indicating Desired Data ======================= |Flake8| inspects the plugin's signature to determine what parameters it -expects using :func:`flake8.utils.parameters_for`. -:attr:`flake8.plugins.manager.Plugin.parameters` caches the values so that +expects using :func:`flake8.plugins.finder._parameters_for`. +:attr:`flake8.plugins.finder.LoadedPlugin.parameters` caches the values so that each plugin makes that fairly expensive call once per plugin. When processing a file, a plugin can ask for any of the following: diff --git a/example-plugin/src/flake8_example_plugin/off_by_default.py b/example-plugin/src/flake8_example_plugin/off_by_default.py index 93dfb38..54737cb 100644 --- a/example-plugin/src/flake8_example_plugin/off_by_default.py +++ b/example-plugin/src/flake8_example_plugin/off_by_default.py @@ -4,9 +4,6 @@ class ExampleTwo: """Second Example Plugin.""" - name = "off-by-default-example-plugin" - version = "1.0.0" - off_by_default = True def __init__(self, tree): diff --git a/example-plugin/src/flake8_example_plugin/on_by_default.py b/example-plugin/src/flake8_example_plugin/on_by_default.py index d712718..a3e5332 100644 --- a/example-plugin/src/flake8_example_plugin/on_by_default.py +++ b/example-plugin/src/flake8_example_plugin/on_by_default.py @@ -4,9 +4,6 @@ class ExampleOne: """First Example Plugin.""" - name = "on-by-default-example-plugin" - version = "1.0.0" - def __init__(self, tree): self.tree = tree diff --git a/pytest.ini b/pytest.ini index 1978251..0301af3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,4 @@ [pytest] -norecursedirs = .git .* *.egg* old docs dist build +norecursedirs = .git .* *.egg* docs dist build addopts = -rw -filterwarnings = - error - ignore:SelectableGroups:DeprecationWarning +filterwarnings = error diff --git a/setup.cfg b/setup.cfg index 47f8feb..9924505 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,7 +43,7 @@ install_requires = pycodestyle>=2.8.0,<2.9.0 pyflakes>=2.4.0,<2.5.0 importlib-metadata<4.3;python_version<"3.8" -python_requires = >=3.6 +python_requires = >=3.6.1 [options.packages.find] where = src @@ -109,12 +109,6 @@ warn_unused_ignores = true # TODO: fix these [mypy-flake8.api.legacy] disallow_untyped_defs = false -[mypy-flake8.checker] -disallow_untyped_defs = false -[mypy-flake8.main.application] -disallow_untyped_defs = false -[mypy-flake8.plugins.manager] -disallow_untyped_defs = false [mypy-tests.*] disallow_untyped_defs = false diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 63fa439..a42523d 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -1,4 +1,5 @@ """Checker Manager and Checker classes.""" +import argparse import collections import errno import itertools @@ -6,6 +7,7 @@ import logging import multiprocessing.pool import signal import tokenize +from typing import Any from typing import Dict from typing import List from typing import Optional @@ -16,6 +18,9 @@ from flake8 import exceptions from flake8 import processor from flake8 import utils from flake8.discover_files import expand_paths +from flake8.plugins.finder import Checkers +from flake8.plugins.finder import LoadedPlugin +from flake8.style_guide import StyleGuideManager Results = List[Tuple[str, int, int, str, Optional[str]]] @@ -56,21 +61,15 @@ class Manager: together and make our output deterministic. """ - def __init__(self, style_guide, checker_plugins): - """Initialize our Manager instance. - - :param style_guide: - The instantiated style guide for this instance of Flake8. - :type style_guide: - flake8.style_guide.StyleGuide - :param checker_plugins: - The plugins representing checks parsed from entry-points. - :type checker_plugins: - flake8.plugins.manager.Checkers - """ + def __init__( + self, + style_guide: StyleGuideManager, + plugins: Checkers, + ) -> None: + """Initialize our Manager instance.""" self.style_guide = style_guide self.options = style_guide.options - self.checks = checker_plugins + self.plugins = plugins self.jobs = self._job_count() self._all_checkers: List[FileChecker] = [] self.checkers: List[FileChecker] = [] @@ -158,9 +157,12 @@ class Manager: if paths is None: paths = self.options.filenames - checks = self.checks.to_dictionary() self._all_checkers = [ - FileChecker(filename, checks, self.options) + FileChecker( + filename=filename, + plugins=self.plugins, + options=self.options, + ) for filename in expand_paths( paths=paths, stdin_display_name=self.options.stdin_display_name, @@ -273,23 +275,17 @@ class Manager: class FileChecker: """Manage running checks for a file and aggregate the results.""" - def __init__(self, filename, checks, options): - """Initialize our file checker. - - :param str filename: - Name of the file to check. - :param checks: - The plugins registered to check the file. - :type checks: - dict - :param options: - Parsed option values from config and command-line. - :type options: - argparse.Namespace - """ + def __init__( + self, + *, + filename: str, + plugins: Checkers, + options: argparse.Namespace, + ) -> None: + """Initialize our file checker.""" self.options = options self.filename = filename - self.checks = checks + self.plugins = plugins self.results: Results = [] self.statistics = { "tokens": 0, @@ -342,29 +338,27 @@ class FileChecker: self.results.append((error_code, line_number, column, text, line)) return error_code - def run_check(self, plugin, **arguments): + def run_check(self, plugin: LoadedPlugin, **arguments: Any) -> Any: """Run the check in a single plugin.""" LOG.debug("Running %r with %r", plugin, arguments) assert self.processor is not None try: - self.processor.keyword_arguments_for( - plugin["parameters"], arguments - ) + self.processor.keyword_arguments_for(plugin.parameters, arguments) except AttributeError as ae: LOG.error("Plugin requested unknown parameters.") raise exceptions.PluginRequestedUnknownParameters( - plugin_name=plugin["plugin_name"], exception=ae + plugin_name=plugin.plugin.package, exception=ae ) try: - return plugin["plugin"](**arguments) + return plugin.obj(**arguments) except Exception as all_exc: LOG.critical( "Plugin %s raised an unexpected exception", - plugin["name"], + plugin.display_name, exc_info=True, ) raise exceptions.PluginExecutionFailed( - plugin_name=plugin["plugin_name"], exception=all_exc + plugin_name=plugin.display_name, exception=all_exc ) @staticmethod @@ -431,7 +425,7 @@ class FileChecker: assert self.processor is not None ast = self.processor.build_ast() - for plugin in self.checks["ast_plugins"]: + for plugin in self.plugins.tree: checker = self.run_check(plugin, tree=ast) # If the plugin uses a class, call the run method of it, otherwise # the call should return something iterable itself @@ -457,7 +451,7 @@ class FileChecker: LOG.debug('Logical line: "%s"', logical_line.rstrip()) - for plugin in self.checks["logical_line_plugins"]: + for plugin in self.plugins.logical_line: self.processor.update_checker_state_for(plugin) results = self.run_check(plugin, logical_line=logical_line) or () for offset, text in results: @@ -479,7 +473,7 @@ class FileChecker: A single physical check may return multiple errors. """ assert self.processor is not None - for plugin in self.checks["physical_line_plugins"]: + for plugin in self.plugins.physical_line: self.processor.update_checker_state_for(plugin) result = self.run_check(plugin, physical_line=physical_line) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 1894881..86fd89f 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -3,7 +3,6 @@ import argparse import configparser import json import logging -import sys import time from typing import Dict from typing import List @@ -12,7 +11,6 @@ from typing import Sequence from typing import Set from typing import Tuple from typing import Type -from typing import TYPE_CHECKING import flake8 from flake8 import checker @@ -20,15 +18,14 @@ from flake8 import defaults from flake8 import exceptions from flake8 import style_guide from flake8 import utils +from flake8.formatting.base import BaseFormatter from flake8.main import debug from flake8.main import options from flake8.options import aggregator from flake8.options import config from flake8.options import manager -from flake8.plugins import manager as plugin_manager - -if TYPE_CHECKING: - from flake8.formatting.base import BaseFormatter +from flake8.plugins import finder +from flake8.plugins import reporter LOG = logging.getLogger(__name__) @@ -56,12 +53,7 @@ class Application: #: to parse and handle the options and arguments passed by the user self.option_manager: Optional[manager.OptionManager] = None - #: The instance of :class:`flake8.plugins.manager.Checkers` - self.check_plugins: Optional[plugin_manager.Checkers] = None - #: The instance of :class:`flake8.plugins.manager.ReportFormatters` - self.formatting_plugins: Optional[ - plugin_manager.ReportFormatters - ] = None + self.plugins: Optional[finder.Plugins] = None #: The user-selected formatter from :attr:`formatting_plugins` self.formatter: Optional[BaseFormatter] = None #: The :class:`flake8.style_guide.StyleGuideManager` built from the @@ -130,49 +122,23 @@ class Application: ) -> None: """Find and load the plugins for this application. - Set the :attr:`check_plugins` and :attr:`formatting_plugins` attributes - based on the discovered plugins found. + Set :attr:`plugins` based on loaded plugins. """ - # TODO: move to src/flake8/plugins/finder.py - extension_local = utils.parse_comma_separated_list( - cfg.get("flake8:local-plugins", "extension", fallback="").strip(), - regexp=utils.LOCAL_PLUGIN_LIST_RE, - ) - report_local = utils.parse_comma_separated_list( - cfg.get("flake8:local-plugins", "report", fallback="").strip(), - regexp=utils.LOCAL_PLUGIN_LIST_RE, - ) - - paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() - local_paths = utils.parse_comma_separated_list(paths_s) - local_paths = utils.normalize_paths(local_paths, cfg_dir) - - sys.path.extend(local_paths) - - self.check_plugins = plugin_manager.Checkers(extension_local) - - self.formatting_plugins = plugin_manager.ReportFormatters(report_local) - - self.check_plugins.load_plugins() - self.formatting_plugins.load_plugins() + raw_plugins = finder.find_plugins(cfg) + local_plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) + self.plugins = finder.load_plugins(raw_plugins, local_plugin_paths) def register_plugin_options(self) -> None: """Register options provided by plugins to our option manager.""" - assert self.check_plugins is not None - assert self.formatting_plugins is not None + assert self.plugins is not None - versions = sorted(set(self.check_plugins.manager.versions())) self.option_manager = manager.OptionManager( version=flake8.__version__, - plugin_versions=", ".join( - f"{name}: {version}" for name, version in versions - ), + plugin_versions=self.plugins.versions_str(), parents=[self.prelim_arg_parser], ) options.register_default_options(self.option_manager) - - self.check_plugins.register_options(self.option_manager) - self.formatting_plugins.register_options(self.option_manager) + self.option_manager.register_plugins(self.plugins) def parse_configuration_and_cli( self, @@ -182,6 +148,7 @@ class Application: ) -> None: """Parse configuration files and the CLI options.""" assert self.option_manager is not None + assert self.plugins is not None self.options = aggregator.aggregate_options( self.option_manager, cfg, @@ -190,8 +157,7 @@ class Application: ) if self.options.bug_report: - assert self.check_plugins is not None - info = debug.information(flake8.__version__, self.check_plugins) + info = debug.information(flake8.__version__, self.plugins) print(json.dumps(info, indent=2, sort_keys=True)) raise SystemExit(0) @@ -202,44 +168,28 @@ class Application: ) self.parsed_diff = utils.parse_unified_diff() - assert self.check_plugins is not None - self.check_plugins.provide_options( - self.option_manager, self.options, self.options.filenames - ) - assert self.formatting_plugins is not None - self.formatting_plugins.provide_options( - self.option_manager, self.options, self.options.filenames - ) + for loaded in self.plugins.all_plugins(): + parse_options = getattr(loaded.obj, "parse_options", None) + if parse_options is None: + continue - def formatter_for(self, formatter_plugin_name): - """Retrieve the formatter class by plugin name.""" - assert self.formatting_plugins is not None - default_formatter = self.formatting_plugins["default"] - formatter_plugin = self.formatting_plugins.get(formatter_plugin_name) - if formatter_plugin is None: - LOG.warning( - '"%s" is an unknown formatter. Falling back to default.', - formatter_plugin_name, - ) - formatter_plugin = default_formatter - - return formatter_plugin.execute + # XXX: ideally we would't have two forms of parse_options + try: + parse_options( + self.option_manager, + self.options, + self.options.filenames, + ) + except TypeError: + parse_options(self.options) def make_formatter( - self, formatter_class: Optional[Type["BaseFormatter"]] = None + self, formatter_class: Optional[Type[BaseFormatter]] = None ) -> None: """Initialize a formatter based on the parsed options.""" + assert self.plugins is not None assert self.options is not None - format_plugin = self.options.format - if 1 <= self.options.quiet < 2: - format_plugin = "quiet-filename" - elif 2 <= self.options.quiet: - format_plugin = "quiet-nothing" - - if formatter_class is None: - formatter_class = self.formatter_for(format_plugin) - - self.formatter = formatter_class(self.options) + self.formatter = reporter.make(self.plugins.reporters, self.options) def make_guide(self) -> None: """Initialize our StyleGuide.""" @@ -254,9 +204,11 @@ class Application: def make_file_checker_manager(self) -> None: """Initialize our FileChecker Manager.""" + assert self.guide is not None + assert self.plugins is not None self.file_checker_manager = checker.Manager( style_guide=self.guide, - checker_plugins=self.check_plugins, + plugins=self.plugins.checkers, ) def run_checks(self) -> None: diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index 4cd9024..03671bc 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -2,30 +2,28 @@ import platform from typing import Any from typing import Dict -from typing import List -from flake8.plugins.manager import PluginTypeManager +from flake8.plugins.finder import Plugins -def information( - version: str, - plugins: PluginTypeManager, -) -> Dict[str, Any]: +def information(version: str, plugins: Plugins) -> Dict[str, Any]: """Generate the information to be printed for the bug report.""" + versions = sorted( + { + (loaded.plugin.package, loaded.plugin.version) + for loaded in plugins.all_plugins() + if loaded.plugin.package not in {"flake8", "local"} + } + ) return { "version": version, - "plugins": plugins_from(plugins), + "plugins": [ + {"plugin": plugin, "version": version} + for plugin, version in versions + ], "platform": { "python_implementation": platform.python_implementation(), "python_version": platform.python_version(), "system": platform.system(), }, } - - -def plugins_from(plugins: PluginTypeManager) -> List[Dict[str, str]]: - """Generate the list of plugins installed.""" - return [ - {"plugin": name, "version": version} - for name, version in sorted(set(plugins.manager.versions())) - ] diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 5d48ba9..d448e1b 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -1,13 +1,11 @@ """Option handling and Option management logic.""" import argparse -import contextlib import enum import functools import logging from typing import Any from typing import Callable from typing import Dict -from typing import Generator from typing import List from typing import Mapping from typing import Optional @@ -18,6 +16,7 @@ from typing import Type from typing import Union from flake8 import utils +from flake8.plugins.finder import Plugins LOG = logging.getLogger(__name__) @@ -351,6 +350,7 @@ class OptionManager: ), ) self.parser.add_argument("filenames", nargs="*", metavar="filename") + self.config_options_dict: Dict[str, Option] = {} self.options: List[Option] = [] self.extended_default_ignore: Set[str] = set() @@ -358,15 +358,32 @@ class OptionManager: self._current_group: Optional[argparse._ArgumentGroup] = None - @contextlib.contextmanager - def group(self, name: str) -> Generator[None, None, None]: - """Attach options to an argparse group during this context.""" - group = self.parser.add_argument_group(name) - self._current_group, orig_group = group, self._current_group - try: - yield - finally: - self._current_group = orig_group + # TODO: maybe make this a free function to reduce api surface area + def register_plugins(self, plugins: Plugins) -> None: + """Register the plugin options (if needed).""" + groups: Dict[str, argparse._ArgumentGroup] = {} + + def _set_group(name: str) -> None: + try: + self._current_group = groups[name] + except KeyError: + group = self.parser.add_argument_group(name) + self._current_group = groups[name] = group + + for loaded in plugins.all_plugins(): + add_options = getattr(loaded.obj, "add_options", None) + if add_options: + _set_group(loaded.plugin.package) + add_options(self) + + # if the plugin is off by default, disable it! + if getattr(loaded.obj, "off_by_default", False): + self.extend_default_ignore(loaded.entry_name) + else: + self.extend_default_select(loaded.entry_name) + + # isn't strictly necessary, but seems cleaner + self._current_group = None def add_option(self, *args: Any, **kwargs: Any) -> None: """Create and register a new option. diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py new file mode 100644 index 0000000..4609474 --- /dev/null +++ b/src/flake8/plugins/finder.py @@ -0,0 +1,256 @@ +"""Functions related to finding and loading plugins.""" +import configparser +import inspect +import logging +import sys +from typing import Any +from typing import Dict +from typing import Generator +from typing import Iterable +from typing import List +from typing import NamedTuple + +from flake8 import utils +from flake8._compat import importlib_metadata +from flake8.exceptions import FailedToLoadPlugin + +LOG = logging.getLogger(__name__) + +FLAKE8_GROUPS = frozenset(("flake8.extension", "flake8.report")) + +BANNED_PLUGINS = { + "flake8-colors": "4.1", + "flake8-per-file-ignores": "3.7", +} + + +class Plugin(NamedTuple): + """A plugin before loading.""" + + package: str + version: str + entry_point: importlib_metadata.EntryPoint + + +class LoadedPlugin(NamedTuple): + """Represents a plugin after being imported.""" + + plugin: Plugin + obj: Any + parameters: Dict[str, bool] + + @property + def entry_name(self) -> str: + """Return the name given in the packaging metadata.""" + return self.plugin.entry_point.name + + @property + def display_name(self) -> str: + """Return the name for use in user-facing / error messages.""" + return f"{self.plugin.package}[{self.entry_name}]" + + +class Checkers(NamedTuple): + """Classified plugins needed for checking.""" + + tree: List[LoadedPlugin] + logical_line: List[LoadedPlugin] + physical_line: List[LoadedPlugin] + + +class Plugins(NamedTuple): + """Classified plugins.""" + + checkers: Checkers + reporters: Dict[str, LoadedPlugin] + + def all_plugins(self) -> Generator[LoadedPlugin, None, None]: + """Return an iterator over all :class:`LoadedPlugin`s.""" + yield from self.checkers.tree + yield from self.checkers.logical_line + yield from self.checkers.physical_line + yield from self.reporters.values() + + def versions_str(self) -> str: + """Return a user-displayed list of plugin versions.""" + return ", ".join( + sorted( + { + f"{loaded.plugin.package}: {loaded.plugin.version}" + for loaded in self.all_plugins() + if loaded.plugin.package not in {"flake8", "local"} + } + ) + ) + + +def _flake8_plugins( + eps: Iterable[importlib_metadata.EntryPoint], + name: str, + version: str, +) -> Generator[Plugin, None, None]: + pyflakes_meta = importlib_metadata.distribution("pyflakes").metadata + pycodestyle_meta = importlib_metadata.distribution("pycodestyle").metadata + + for ep in eps: + if ep.group not in FLAKE8_GROUPS: + continue + + if ep.name == "F": + yield Plugin(pyflakes_meta["name"], pyflakes_meta["version"], ep) + elif ep.name.startswith("pycodestyle"): + yield Plugin( + pycodestyle_meta["name"], pycodestyle_meta["version"], ep + ) + else: + yield Plugin(name, version, ep) + + +def _find_importlib_plugins() -> Generator[Plugin, None, None]: + for dist in importlib_metadata.distributions(): + # assigned to prevent continual reparsing + eps = dist.entry_points + + # perf: skip parsing `.metadata` (slow) if no entry points match + if not any(ep.group in FLAKE8_GROUPS for ep in eps): + continue + + # assigned to prevent continual reparsing + meta = dist.metadata + + if meta["name"] in BANNED_PLUGINS: + LOG.warning( + "%s plugin is obsolete in flake8>=%s", + meta["name"], + BANNED_PLUGINS[meta["name"]], + ) + continue + elif meta["name"] == "flake8": + # special case flake8 which provides plugins for pyflakes / + # pycodestyle + yield from _flake8_plugins(eps, meta["name"], meta["version"]) + continue + + for ep in eps: + if ep.group in FLAKE8_GROUPS: + yield Plugin(meta["name"], meta["version"], ep) + + +def _find_local_plugins( + cfg: configparser.RawConfigParser, +) -> Generator[Plugin, None, None]: + for plugin_type in ("extension", "report"): + group = f"flake8.{plugin_type}" + for plugin_s in utils.parse_comma_separated_list( + cfg.get("flake8:local-plugins", plugin_type, fallback="").strip(), + regexp=utils.LOCAL_PLUGIN_LIST_RE, + ): + name, _, entry_str = plugin_s.partition("=") + name, entry_str = name.strip(), entry_str.strip() + ep = importlib_metadata.EntryPoint(name, entry_str, group) + yield Plugin("local", "local", ep) + + +def find_plugins(cfg: configparser.RawConfigParser) -> List[Plugin]: + """Discovers all plugins (but does not load them).""" + ret = [*_find_importlib_plugins(), *_find_local_plugins(cfg)] + + # for determinism, sort the list + ret.sort() + + return ret + + +def find_local_plugin_paths( + cfg: configparser.RawConfigParser, + cfg_dir: str, +) -> List[str]: + """Discovers the list of ``flake8:local-plugins`` ``paths``.""" + paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() + paths = utils.parse_comma_separated_list(paths_s) + return utils.normalize_paths(paths, cfg_dir) + + +def _parameters_for(func: Any) -> Dict[str, bool]: + """Return the parameters for the plugin. + + This will inspect the plugin and return either the function parameters + if the plugin is a function or the parameters for ``__init__`` after + ``self`` if the plugin is a class. + + :returns: + A dictionary mapping the parameter name to whether or not it is + required (a.k.a., is positional only/does not have a default). + """ + is_class = not inspect.isfunction(func) + if is_class: + func = func.__init__ + + parameters = { + parameter.name: parameter.default is inspect.Parameter.empty + for parameter in inspect.signature(func).parameters.values() + if parameter.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + } + + if is_class: + parameters.pop("self", None) + + return parameters + + +def _load_plugin(plugin: Plugin) -> LoadedPlugin: + try: + obj = plugin.entry_point.load() + except Exception as e: + raise FailedToLoadPlugin(plugin.package, e) + + if not callable(obj): + err = TypeError("expected loaded plugin to be callable") + raise FailedToLoadPlugin(plugin.package, err) + + return LoadedPlugin(plugin, obj, _parameters_for(obj)) + + +def _import_plugins( + plugins: List[Plugin], paths: List[str] +) -> List[LoadedPlugin]: + sys.path.extend(paths) + return [_load_plugin(p) for p in plugins] + + +def _classify_plugins(plugins: List[LoadedPlugin]) -> Plugins: + tree = [] + logical_line = [] + physical_line = [] + reporters = {} + + for loaded in plugins: + if loaded.plugin.entry_point.group == "flake8.report": + reporters[loaded.entry_name] = loaded + elif "tree" in loaded.parameters: + tree.append(loaded) + elif "logical_line" in loaded.parameters: + logical_line.append(loaded) + elif "physical_line" in loaded.parameters: + physical_line.append(loaded) + else: + raise NotImplementedError(f"what plugin type? {loaded}") + + return Plugins( + checkers=Checkers( + tree=tree, + logical_line=logical_line, + physical_line=physical_line, + ), + reporters=reporters, + ) + + +def load_plugins(plugins: List[Plugin], paths: List[str]) -> Plugins: + """Load and classify all flake8 plugins. + + - first: extends ``sys.path`` with ``paths`` (to import local plugins) + - next: converts the ``Plugin``s to ``LoadedPlugins`` + - finally: classifies plugins into their specific types + """ + return _classify_plugins(_import_plugins(plugins, paths)) diff --git a/src/flake8/plugins/manager.py b/src/flake8/plugins/manager.py deleted file mode 100644 index fe9b038..0000000 --- a/src/flake8/plugins/manager.py +++ /dev/null @@ -1,530 +0,0 @@ -"""Plugin loading and management logic and classes.""" -import logging -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set - -from flake8 import exceptions -from flake8 import utils -from flake8._compat import importlib_metadata - -LOG = logging.getLogger(__name__) - -__all__ = ("Checkers", "Plugin", "PluginManager", "ReportFormatters") - -NO_GROUP_FOUND = object() - - -class Plugin: - """Wrap an EntryPoint from setuptools and other logic.""" - - def __init__(self, name, entry_point, local=False): - """Initialize our Plugin. - - :param str name: - Name of the entry-point as it was registered with setuptools. - :param entry_point: - EntryPoint returned by setuptools. - :type entry_point: - setuptools.EntryPoint - :param bool local: - Is this a repo-local plugin? - """ - self.name = name - self.entry_point = entry_point - self.local = local - self._plugin: Any = None - self._parameters = None - self._parameter_names: Optional[List[str]] = None - self._group = None - self._plugin_name = None - self._version = None - - def __repr__(self) -> str: - """Provide an easy to read description of the current plugin.""" - return 'Plugin(name="{}", entry_point="{}")'.format( - self.name, self.entry_point.value - ) - - def to_dictionary(self): - """Convert this plugin to a dictionary.""" - return { - "name": self.name, - "parameters": self.parameters, - "parameter_names": self.parameter_names, - "plugin": self.plugin, - "plugin_name": self.plugin_name, - } - - def is_in_a_group(self): - """Determine if this plugin is in a group. - - :returns: - True if the plugin is in a group, otherwise False. - :rtype: - bool - """ - return self.group() is not None - - def group(self): - """Find and parse the group the plugin is in.""" - if self._group is None: - name = self.name.split(".", 1) - if len(name) > 1: - self._group = name[0] - else: - self._group = NO_GROUP_FOUND - if self._group is NO_GROUP_FOUND: - return None - return self._group - - @property - def parameters(self): - """List of arguments that need to be passed to the plugin.""" - if self._parameters is None: - self._parameters = utils.parameters_for(self) - return self._parameters - - @property - def parameter_names(self) -> List[str]: - """List of argument names that need to be passed to the plugin.""" - if self._parameter_names is None: - self._parameter_names = list(self.parameters) - return self._parameter_names - - @property - def plugin(self): - """Load and return the plugin associated with the entry-point. - - This property implicitly loads the plugin and then caches it. - """ - self.load_plugin() - return self._plugin - - @property - def version(self) -> str: - """Return the version of the plugin.""" - version = self._version - if version is None: - if self.is_in_a_group(): - version = self._version = version_for(self) - else: - version = self._version = self.plugin.version - return version - - @property - def plugin_name(self): - """Return the name of the plugin.""" - if self._plugin_name is None: - if self.is_in_a_group(): - self._plugin_name = self.group() - else: - self._plugin_name = self.plugin.name - - return self._plugin_name - - @property - def off_by_default(self): - """Return whether the plugin is ignored by default.""" - return getattr(self.plugin, "off_by_default", False) - - def execute(self, *args, **kwargs): - r"""Call the plugin with \*args and \*\*kwargs.""" - return self.plugin(*args, **kwargs) # pylint: disable=not-callable - - def _load(self): - self._plugin = self.entry_point.load() - if not callable(self._plugin): - msg = ( - f"Plugin {self._plugin!r} is not a callable. It might be " - f"written for an older version of flake8 and might not work " - f"with this version" - ) - LOG.critical(msg) - raise TypeError(msg) - - def load_plugin(self): - """Retrieve the plugin for this entry-point. - - This loads the plugin, stores it on the instance and then returns it. - It does not reload it after the first time, it merely returns the - cached plugin. - - :returns: - Nothing - """ - if self._plugin is None: - LOG.info('Loading plugin "%s" from entry-point.', self.name) - try: - self._load() - except Exception as load_exception: - LOG.exception(load_exception) - failed_to_load = exceptions.FailedToLoadPlugin( - plugin_name=self.name, exception=load_exception - ) - LOG.critical(str(failed_to_load)) - raise failed_to_load - - def enable(self, optmanager, options=None): - """Remove plugin name from the default ignore list.""" - optmanager.remove_from_default_ignore([self.name]) - optmanager.extend_default_select([self.name]) - if not options: - return - try: - options.ignore.remove(self.name) - except (ValueError, KeyError): - LOG.debug( - "Attempted to remove %s from the ignore list but it was " - "not a member of the list.", - self.name, - ) - - def disable(self, optmanager): - """Add the plugin name to the default ignore list.""" - optmanager.extend_default_ignore([self.name]) - - def provide_options(self, optmanager, options, extra_args): - """Pass the parsed options and extra arguments to the plugin.""" - parse_options = getattr(self.plugin, "parse_options", None) - if parse_options is not None: - LOG.debug('Providing options to plugin "%s".', self.name) - try: - parse_options(optmanager, options, extra_args) - except TypeError: - parse_options(options) - - if self.name in options.enable_extensions: - self.enable(optmanager, options) - - def register_options(self, optmanager): - """Register the plugin's command-line options on the OptionManager. - - :param optmanager: - Instantiated OptionManager to register options on. - :type optmanager: - flake8.options.manager.OptionManager - :returns: - Nothing - """ - add_options = getattr(self.plugin, "add_options", None) - if add_options is not None: - LOG.debug( - 'Registering options from plugin "%s" on OptionManager %r', - self.name, - optmanager, - ) - with optmanager.group(self.plugin_name): - add_options(optmanager) - - if self.off_by_default: - self.disable(optmanager) - - -class PluginManager: # pylint: disable=too-few-public-methods - """Find and manage plugins consistently.""" - - def __init__( - self, namespace: str, local_plugins: Optional[List[str]] = None - ) -> None: - """Initialize the manager. - - :param str namespace: - Namespace of the plugins to manage, e.g., 'flake8.extension'. - :param list local_plugins: - Plugins from config (as "X = path.to:Plugin" strings). - """ - self.namespace = namespace - self.plugins: Dict[str, Plugin] = {} - self.names: List[str] = [] - self._load_local_plugins(local_plugins or []) - self._load_entrypoint_plugins() - - def _load_local_plugins(self, local_plugins): - """Load local plugins from config. - - :param list local_plugins: - Plugins from config (as "X = path.to:Plugin" strings). - """ - for plugin_str in local_plugins: - name, _, entry_str = plugin_str.partition("=") - name, entry_str = name.strip(), entry_str.strip() - entry_point = importlib_metadata.EntryPoint( - name, entry_str, self.namespace - ) - self._load_plugin_from_entrypoint(entry_point, local=True) - - def _load_entrypoint_plugins(self): - LOG.info('Loading entry-points for "%s".', self.namespace) - eps = importlib_metadata.entry_points().get(self.namespace, ()) - # python2.7 occasionally gives duplicate results due to redundant - # `local/lib` -> `../lib` symlink on linux in virtualenvs so we - # eliminate duplicates here - for entry_point in sorted(frozenset(eps)): - if entry_point.name == "per-file-ignores": - LOG.warning( - "flake8-per-file-ignores plugin is incompatible with " - "flake8>=3.7 (which implements per-file-ignores itself)." - ) - continue - elif entry_point.name == "flake8-colors": - LOG.warning( - "flake8-colors plugin is incompatible with " - "flake8>=4.1 (which implements colors itself)." - ) - continue - self._load_plugin_from_entrypoint(entry_point) - - def _load_plugin_from_entrypoint(self, entry_point, local=False): - """Load a plugin from a setuptools EntryPoint. - - :param EntryPoint entry_point: - EntryPoint to load plugin from. - :param bool local: - Is this a repo-local plugin? - """ - name = entry_point.name - self.plugins[name] = Plugin(name, entry_point, local=local) - self.names.append(name) - LOG.debug('Loaded %r for plugin "%s".', self.plugins[name], name) - - def map(self, func, *args, **kwargs): - r"""Call ``func`` with the plugin and \*args and \**kwargs after. - - This yields the return value from ``func`` for each plugin. - - :param collections.Callable func: - Function to call with each plugin. Signature should at least be: - - .. code-block:: python - - def myfunc(plugin): - pass - - Any extra positional or keyword arguments specified with map will - be passed along to this function after the plugin. The plugin - passed is a :class:`~flake8.plugins.manager.Plugin`. - :param args: - Positional arguments to pass to ``func`` after each plugin. - :param kwargs: - Keyword arguments to pass to ``func`` after each plugin. - """ - for name in self.names: - yield func(self.plugins[name], *args, **kwargs) - - def versions(self): - # () -> (str, str) - """Generate the versions of plugins. - - :returns: - Tuples of the plugin_name and version - :rtype: - tuple - """ - plugins_seen: Set[str] = set() - for entry_point_name in self.names: - plugin = self.plugins[entry_point_name] - plugin_name = plugin.plugin_name - if plugin.plugin_name in plugins_seen: - continue - plugins_seen.add(plugin_name) - yield (plugin_name, plugin.version) - - -def version_for(plugin): - # (Plugin) -> Optional[str] - """Determine the version of a plugin by its module. - - :param plugin: - The loaded plugin - :type plugin: - Plugin - :returns: - version string for the module - :rtype: - str - """ - module_name = plugin.plugin.__module__ - try: - module = __import__(module_name) - except ImportError: - return None - - return getattr(module, "__version__", None) - - -class PluginTypeManager: - """Parent class for most of the specific plugin types.""" - - namespace: str - - def __init__(self, local_plugins=None): - """Initialize the plugin type's manager. - - :param list local_plugins: - Plugins from config file instead of entry-points - """ - self.manager = PluginManager( - self.namespace, local_plugins=local_plugins - ) - self.plugins_loaded = False - - def __contains__(self, name): - """Check if the entry-point name is in this plugin type manager.""" - LOG.debug('Checking for "%s" in plugin type manager.', name) - return name in self.plugins - - def __getitem__(self, name): - """Retrieve a plugin by its name.""" - LOG.debug('Retrieving plugin for "%s".', name) - return self.plugins[name] - - def get(self, name, default=None): - """Retrieve the plugin referred to by ``name`` or return the default. - - :param str name: - Name of the plugin to retrieve. - :param default: - Default value to return. - :returns: - Plugin object referred to by name, if it exists. - :rtype: - :class:`Plugin` - """ - if name in self: - return self[name] - return default - - @property - def names(self): - """Proxy attribute to underlying manager.""" - return self.manager.names - - @property - def plugins(self): - """Proxy attribute to underlying manager.""" - return self.manager.plugins - - @staticmethod - def _generate_call_function(method_name, optmanager, *args, **kwargs): - def generated_function(plugin): - method = getattr(plugin, method_name, None) - if method is not None and callable(method): - return method(optmanager, *args, **kwargs) - - return generated_function - - def load_plugins(self): - """Load all plugins of this type that are managed by this manager.""" - if self.plugins_loaded: - return - - for plugin in self.plugins.values(): - plugin.load_plugin() - - # Do not set plugins_loaded if we run into an exception - self.plugins_loaded = True - - def register_options(self, optmanager): - """Register all of the checkers' options to the OptionManager.""" - self.load_plugins() - call_register_options = self._generate_call_function( - "register_options", optmanager - ) - - list(self.manager.map(call_register_options)) - - def provide_options(self, optmanager, options, extra_args): - """Provide parsed options and extra arguments to the plugins.""" - call_provide_options = self._generate_call_function( - "provide_options", optmanager, options, extra_args - ) - - list(self.manager.map(call_provide_options)) - - -class Checkers(PluginTypeManager): - """All of the checkers registered through entry-points or config.""" - - namespace = "flake8.extension" - - def checks_expecting(self, argument_name): - """Retrieve checks that expect an argument with the specified name. - - Find all checker plugins that are expecting a specific argument. - """ - for plugin in self.plugins.values(): - if argument_name == plugin.parameter_names[0]: - yield plugin - - def to_dictionary(self): - """Return a dictionary of AST and line-based plugins.""" - return { - "ast_plugins": [ - plugin.to_dictionary() for plugin in self.ast_plugins - ], - "logical_line_plugins": [ - plugin.to_dictionary() for plugin in self.logical_line_plugins - ], - "physical_line_plugins": [ - plugin.to_dictionary() for plugin in self.physical_line_plugins - ], - } - - def register_options(self, optmanager): - """Register all of the checkers' options to the OptionManager. - - This also ensures that plugins that are not part of a group and are - enabled by default are enabled on the option manager. - """ - # NOTE(sigmavirus24) We reproduce a little of - # PluginTypeManager.register_options to reduce the number of times - # that we loop over the list of plugins. Instead of looping twice, - # option registration and enabling the plugin, we loop once with one - # function to map over the plugins. - self.load_plugins() - call_register_options = self._generate_call_function( - "register_options", optmanager - ) - - def register_and_enable(plugin): - call_register_options(plugin) - if plugin.group() is None and not plugin.off_by_default: - plugin.enable(optmanager) - - list(self.manager.map(register_and_enable)) - - @property - def ast_plugins(self): - """List of plugins that expect the AST tree.""" - plugins = getattr(self, "_ast_plugins", []) - if not plugins: - plugins = list(self.checks_expecting("tree")) - self._ast_plugins = plugins - return plugins - - @property - def logical_line_plugins(self): - """List of plugins that expect the logical lines.""" - plugins = getattr(self, "_logical_line_plugins", []) - if not plugins: - plugins = list(self.checks_expecting("logical_line")) - self._logical_line_plugins = plugins - return plugins - - @property - def physical_line_plugins(self): - """List of plugins that expect the physical lines.""" - plugins = getattr(self, "_physical_line_plugins", []) - if not plugins: - plugins = list(self.checks_expecting("physical_line")) - self._physical_line_plugins = plugins - return plugins - - -class ReportFormatters(PluginTypeManager): - """All of the report formatters registered through entry-points/config.""" - - namespace = "flake8.report" diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 7509438..ba3493a 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -69,8 +69,6 @@ FLAKE8_PYFLAKES_CODES = { class FlakesChecker(pyflakes.checker.Checker): """Subclass the Pyflakes checker to conform with the flake8 API.""" - name = "pyflakes" - version = pyflakes.__version__ with_doctest = False include_in_doctest: List[str] = [] exclude_from_doctest: List[str] = [] diff --git a/src/flake8/plugins/reporter.py b/src/flake8/plugins/reporter.py new file mode 100644 index 0000000..5bbbd81 --- /dev/null +++ b/src/flake8/plugins/reporter.py @@ -0,0 +1,41 @@ +"""Functions for construcing the requested report plugin.""" +import argparse +import logging +from typing import Dict + +from flake8.formatting.base import BaseFormatter +from flake8.plugins.finder import LoadedPlugin + +LOG = logging.getLogger(__name__) + + +def make( + reporters: Dict[str, LoadedPlugin], + options: argparse.Namespace, +) -> BaseFormatter: + """Make the formatter from the requested user options. + + - if :option:`flake8 --quiet` is specified, return the ``quiet-filename`` + formatter. + - if :option:`flake8 --quiet` is specified at least twice, return the + ``quiet-nothing`` formatter. + - otherwise attempt to return the formatter by name. + - failing that, assume it is a format string and return the ``default`` + formatter. + """ + format_name = options.format + if options.quiet == 1: + format_name = "quiet-filename" + elif options.quiet >= 2: + format_name = "quiet-nothing" + + try: + format_plugin = reporters[format_name] + except KeyError: + LOG.warning( + "%r is an unknown formatter. Falling back to default.", + format_name, + ) + format_plugin = reporters["default"] + + return format_plugin.obj(options) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 2649c9d..d0652d8 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -14,6 +14,7 @@ from typing import Tuple import flake8 from flake8 import defaults from flake8 import utils +from flake8.plugins.finder import LoadedPlugin LOG = logging.getLogger(__name__) NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE]) @@ -160,11 +161,11 @@ class FileProcessor: if self.blank_before < self.blank_lines: self.blank_before = self.blank_lines - def update_checker_state_for(self, plugin: Dict[str, Any]) -> None: + def update_checker_state_for(self, plugin: LoadedPlugin) -> None: """Update the checker_state attribute for the plugin.""" - if "checker_state" in plugin["parameters"]: + if "checker_state" in plugin.parameters: self.checker_state = self._checker_states.setdefault( - plugin["name"], {} + plugin.entry_name, {} ) def next_logical_line(self) -> None: diff --git a/src/flake8/utils.py b/src/flake8/utils.py index 9eb2497..a5a1901 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -2,7 +2,6 @@ import collections import fnmatch as _fnmatch import functools -import inspect import io import logging import os @@ -18,14 +17,10 @@ from typing import Pattern from typing import Sequence from typing import Set from typing import Tuple -from typing import TYPE_CHECKING from typing import Union from flake8 import exceptions -if TYPE_CHECKING: - from flake8.plugins.manager import Plugin - 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]") @@ -310,40 +305,6 @@ def fnmatch(filename: str, patterns: Sequence[str]) -> bool: return any(_fnmatch.fnmatch(filename, pattern) for pattern in patterns) -def parameters_for(plugin: "Plugin") -> Dict[str, bool]: - """Return the parameters for the plugin. - - This will inspect the plugin and return either the function parameters - if the plugin is a function or the parameters for ``__init__`` after - ``self`` if the plugin is a class. - - :param plugin: - The internal plugin object. - :type plugin: - flake8.plugins.manager.Plugin - :returns: - A dictionary mapping the parameter name to whether or not it is - required (a.k.a., is positional only/does not have a default). - :rtype: - dict([(str, bool)]) - """ - func = plugin.plugin - is_class = not inspect.isfunction(func) - if is_class: # The plugin is a class - func = plugin.plugin.__init__ - - parameters = { - parameter.name: parameter.default is parameter.empty - for parameter in inspect.signature(func).parameters.values() - if parameter.kind == parameter.POSITIONAL_OR_KEYWORD - } - - if is_class: - parameters.pop("self", None) - - return parameters - - def matches_filename( path: str, patterns: Sequence[str], diff --git a/tests/fixtures/config_files/README.rst b/tests/fixtures/config_files/README.rst deleted file mode 100644 index 8796051..0000000 --- a/tests/fixtures/config_files/README.rst +++ /dev/null @@ -1,22 +0,0 @@ -About this directory -==================== - -The files in this directory are test fixtures for unit and integration tests. -Their purpose is described below. Please note the list of file names that can -not be created as they are already used by tests. - -New fixtures are preferred over updating existing features unless existing -tests will fail. - -Files that should not be created --------------------------------- - -- ``tests/fixtures/config_files/missing.ini`` - -Purposes of existing fixtures ------------------------------ - -``tests/fixtures/config_files/local-plugin.ini`` - - This is for testing configuring a plugin via flake8 config file instead of - setuptools entry-point. diff --git a/tests/fixtures/config_files/local-plugin-path.ini b/tests/fixtures/config_files/local-plugin-path.ini deleted file mode 100644 index 7368c1e..0000000 --- a/tests/fixtures/config_files/local-plugin-path.ini +++ /dev/null @@ -1,5 +0,0 @@ -[flake8:local-plugins] -extension = - XE = aplugin:ExtensionTestPlugin2 -paths = - ../../integration/subdir/ diff --git a/tests/fixtures/config_files/local-plugin.ini b/tests/fixtures/config_files/local-plugin.ini deleted file mode 100644 index 8344f76..0000000 --- a/tests/fixtures/config_files/local-plugin.ini +++ /dev/null @@ -1,5 +0,0 @@ -[flake8:local-plugins] -extension = - XE = tests.integration.test_plugins:ExtensionTestPlugin -report = - XR = tests.integration.test_plugins:ReportTestPlugin diff --git a/tests/integration/subdir/aplugin.py b/tests/integration/subdir/aplugin.py index fde5890..801f2c0 100644 --- a/tests/integration/subdir/aplugin.py +++ b/tests/integration/subdir/aplugin.py @@ -4,9 +4,6 @@ class ExtensionTestPlugin2: """Extension test plugin in its own directory.""" - name = "ExtensionTestPlugin2" - version = "1.0.0" - def __init__(self, tree): """Construct an instance of test plugin.""" diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 29db6d3..96b7d4b 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -6,28 +6,19 @@ import pytest from flake8 import checker from flake8._compat import importlib_metadata -from flake8.plugins import manager +from flake8.plugins import finder from flake8.processor import FileProcessor PHYSICAL_LINE = "# Physical line content" EXPECTED_REPORT = (1, 1, "T000 Expected Message") EXPECTED_REPORT_PHYSICAL_LINE = (1, "T000 Expected Message") -EXPECTED_RESULT_PHYSICAL_LINE = ( - "T000", - 0, - 1, - "Expected Message", - None, -) +EXPECTED_RESULT_PHYSICAL_LINE = ("T000", 0, 1, "Expected Message", None) class PluginClass: """Simple file plugin class yielding the expected report.""" - name = "test" - version = "1.0.0" - def __init__(self, tree): """Construct a dummy object to provide mandatory parameter.""" pass @@ -37,87 +28,78 @@ class PluginClass: yield EXPECTED_REPORT + (type(self),) -def plugin_func(func): - """Decorate file plugins which are implemented as functions.""" - func.name = "test" - func.version = "1.0.0" - return func - - -@plugin_func def plugin_func_gen(tree): """Yield the expected report.""" yield EXPECTED_REPORT + (type(plugin_func_gen),) -@plugin_func def plugin_func_list(tree): """Return a list of expected reports.""" return [EXPECTED_REPORT + (type(plugin_func_list),)] -@plugin_func def plugin_func_physical_ret(physical_line): """Expect report from a physical_line. Single return.""" return EXPECTED_REPORT_PHYSICAL_LINE -@plugin_func def plugin_func_physical_none(physical_line): """Expect report from a physical_line. No results.""" return None -@plugin_func def plugin_func_physical_list_single(physical_line): """Expect report from a physical_line. List of single result.""" return [EXPECTED_REPORT_PHYSICAL_LINE] -@plugin_func def plugin_func_physical_list_multiple(physical_line): """Expect report from a physical_line. List of multiple results.""" return [EXPECTED_REPORT_PHYSICAL_LINE] * 2 -@plugin_func def plugin_func_physical_gen_single(physical_line): """Expect report from a physical_line. Generator of single result.""" yield EXPECTED_REPORT_PHYSICAL_LINE -@plugin_func def plugin_func_physical_gen_multiple(physical_line): """Expect report from a physical_line. Generator of multiple results.""" for _ in range(3): yield EXPECTED_REPORT_PHYSICAL_LINE +def plugin_func_out_of_bounds(logical_line): + """This produces an error out of bounds.""" + yield 10000, "L100 test" + + def mock_file_checker_with_plugin(plugin_target): """Get a mock FileChecker class with plugin_target registered. Useful as a starting point for mocking reports/results. """ - # Mock an entry point returning the plugin target - entry_point = mock.Mock(spec=["load"]) - entry_point.name = plugin_target.name - entry_point.load.return_value = plugin_target - entry_point.value = "mocked:value" - - # Load the checker plugins using the entry point mock - with mock.patch.object( - importlib_metadata, - "entry_points", - return_value={"flake8.extension": [entry_point]}, - ): - checks = manager.Checkers() + to_load = [ + finder.Plugin( + "flake-package", + "9001", + importlib_metadata.EntryPoint( + "Q", + f"{plugin_target.__module__}:{plugin_target.__name__}", + "flake8.extension", + ), + ), + ] + plugins = finder.load_plugins(to_load, []) # Prevent it from reading lines from stdin or somewhere else with mock.patch( "flake8.processor.FileProcessor.read_lines", return_value=["Line 1"] ): file_checker = checker.FileChecker( - "-", checks.to_dictionary(), mock.MagicMock() + filename="-", + plugins=plugins.checkers, + options=mock.MagicMock(), ) return file_checker @@ -173,11 +155,7 @@ def test_line_check_results(plugin_target, len_results): def test_logical_line_offset_out_of_bounds(): """Ensure that logical line offsets that are out of bounds do not crash.""" - @plugin_func - def _logical_line_out_of_bounds(logical_line): - yield 10000, "L100 test" - - file_checker = mock_file_checker_with_plugin(_logical_line_out_of_bounds) + file_checker = mock_file_checker_with_plugin(plugin_func_out_of_bounds) logical_ret = ( "", @@ -293,7 +271,7 @@ def test_report_order(results, expected_order): # Create a placeholder manager without arguments or plugins # Just add one custom file checker which just provides the results - manager = checker.Manager(style_guide, []) + manager = checker.Manager(style_guide, finder.Checkers([], [], [])) manager.checkers = manager._all_checkers = [file_checker] # _handle_results is the first place which gets the sorted result @@ -357,6 +335,10 @@ def test_handling_syntaxerrors_across_pythons(): "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11) ) expected = (2, 1) - file_checker = checker.FileChecker("-", {}, mock.MagicMock()) + file_checker = checker.FileChecker( + filename="-", + plugins=finder.Checkers([], [], []), + options=mock.MagicMock(), + ) actual = file_checker._extract_syntax_information(err) assert actual == expected diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 7d28aff..682360c 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -1,17 +1,18 @@ """Integration tests for plugin loading.""" -from flake8.main import application -from flake8.main.cli import main +import pytest -LOCAL_PLUGIN_CONFIG = "tests/fixtures/config_files/local-plugin.ini" -LOCAL_PLUGIN_PATH_CONFIG = "tests/fixtures/config_files/local-plugin-path.ini" +from flake8.main.cli import main +from flake8.main.options import register_default_options +from flake8.main.options import stage1_arg_parser +from flake8.options import aggregator +from flake8.options import config +from flake8.options.manager import OptionManager +from flake8.plugins import finder class ExtensionTestPlugin: """Extension test plugin.""" - name = "ExtensionTestPlugin" - version = "1.0.0" - def __init__(self, tree): """Construct an instance of test plugin.""" @@ -27,9 +28,6 @@ class ExtensionTestPlugin: class ReportTestPlugin: """Report test plugin.""" - name = "ReportTestPlugin" - version = "1.0.0" - def __init__(self, tree): """Construct an instance of test plugin.""" @@ -37,41 +35,69 @@ class ReportTestPlugin: """Do nothing.""" -def test_enable_local_plugin_from_config(): +@pytest.fixture +def local_config(tmp_path): + cfg_s = f"""\ +[flake8:local-plugins] +extension = + XE = {ExtensionTestPlugin.__module__}:{ExtensionTestPlugin.__name__} +report = + XR = {ReportTestPlugin.__module__}:{ReportTestPlugin.__name__} +""" + cfg = tmp_path.joinpath("tox.ini") + cfg.write_text(cfg_s) + + return str(cfg) + + +def test_enable_local_plugin_from_config(local_config): """App can load a local plugin from config file.""" - app = application.Application() - app.initialize(["flake8", "--config", LOCAL_PLUGIN_CONFIG]) + cfg, cfg_dir = config.load_config(local_config, [], isolated=False) + plugins = finder.find_plugins(cfg) + plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) + loaded_plugins = finder.load_plugins(plugins, plugin_paths) - assert app.check_plugins is not None - assert app.check_plugins["XE"].plugin is ExtensionTestPlugin - assert app.formatting_plugins is not None - assert app.formatting_plugins["XR"].plugin is ReportTestPlugin + (custom_extension,) = ( + loaded + for loaded in loaded_plugins.checkers.tree + if loaded.entry_name == "XE" + ) + custom_report = loaded_plugins.reporters["XR"] + + assert custom_extension.obj is ExtensionTestPlugin + assert custom_report.obj is ReportTestPlugin -def test_local_plugin_can_add_option(): +def test_local_plugin_can_add_option(local_config): """A local plugin can add a CLI option.""" - app = application.Application() - app.initialize( - ["flake8", "--config", LOCAL_PLUGIN_CONFIG, "--anopt", "foo"] + + argv = ["--config", local_config, "--anopt", "foo"] + + stage1_parser = stage1_arg_parser() + stage1_args, rest = stage1_parser.parse_known_args(argv) + + cfg, cfg_dir = config.load_config( + config=stage1_args.config, extra=[], isolated=False ) - assert app.options is not None - assert app.options.anopt == "foo" + plugins = finder.find_plugins(cfg) + plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) + loaded_plugins = finder.load_plugins(plugins, plugin_paths) + option_manager = OptionManager( + version="123", + plugin_versions="", + parents=[stage1_parser], + ) + register_default_options(option_manager) + option_manager.register_plugins(loaded_plugins) -def test_enable_local_plugin_at_non_installed_path(): - """Can add a paths option in local-plugins config section for finding.""" - app = application.Application() - app.initialize(["flake8", "--config", LOCAL_PLUGIN_PATH_CONFIG]) + args = aggregator.aggregate_options(option_manager, cfg, cfg_dir, argv) - assert app.check_plugins is not None - assert app.check_plugins["XE"].plugin.name == "ExtensionTestPlugin2" + assert args.anopt == "foo" class AlwaysErrors: - name = "AlwaysError" - version = "1" - def __init__(self, tree): pass diff --git a/tests/unit/plugins/__init__.py b/tests/unit/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py new file mode 100644 index 0000000..993c13c --- /dev/null +++ b/tests/unit/plugins/finder_test.py @@ -0,0 +1,610 @@ +import configparser +import sys +from unittest import mock + +import pytest + +from flake8._compat import importlib_metadata +from flake8.exceptions import FailedToLoadPlugin +from flake8.plugins import finder +from flake8.plugins.pyflakes import FlakesChecker + + +def _ep(name="X", value="dne:dne", group="flake8.extension"): + return importlib_metadata.EntryPoint(name, value, group) + + +def _plugin(package="local", version="local", ep=None): + if ep is None: + ep = _ep() + return finder.Plugin(package, version, ep) + + +def _loaded(plugin=None, obj=None, parameters=None): + if plugin is None: + plugin = _plugin() + if parameters is None: + parameters = {"tree": True} + return finder.LoadedPlugin(plugin, obj, parameters) + + +def test_loaded_plugin_entry_name_vs_display_name(): + loaded = _loaded(_plugin(package="package-name", ep=_ep(name="Q"))) + assert loaded.entry_name == "Q" + assert loaded.display_name == "package-name[Q]" + + +def test_plugins_all_plugins(): + tree_plugin = _loaded(parameters={"tree": True}) + logical_line_plugin = _loaded(parameters={"logical_line": True}) + physical_line_plugin = _loaded(parameters={"physical_line": True}) + report_plugin = _loaded( + plugin=_plugin(ep=_ep(name="R", group="flake8.report")) + ) + + plugins = finder.Plugins( + checkers=finder.Checkers( + tree=[tree_plugin], + logical_line=[logical_line_plugin], + physical_line=[physical_line_plugin], + ), + reporters={"R": report_plugin}, + ) + + assert tuple(plugins.all_plugins()) == ( + tree_plugin, + logical_line_plugin, + physical_line_plugin, + report_plugin, + ) + + +def test_plugins_versions_str(): + plugins = finder.Plugins( + checkers=finder.Checkers( + tree=[_loaded(_plugin(package="pkg1", version="1"))], + logical_line=[_loaded(_plugin(package="pkg2", version="2"))], + physical_line=[_loaded(_plugin(package="pkg1", version="1"))], + ), + reporters={ + # ignore flake8 builtin plugins + "default": _loaded(_plugin(package="flake8")), + # ignore local plugins + "custom": _loaded(_plugin(package="local")), + }, + ) + assert plugins.versions_str() == "pkg1: 1, pkg2: 2" + + +@pytest.fixture +def pyflakes_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: pyflakes +Version: 9000.1.0 +""" + d = tmp_path.joinpath("pyflakes.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + return importlib_metadata.PathDistribution(d) + + +@pytest.fixture +def pycodestyle_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: pycodestyle +Version: 9000.2.0 +""" + d = tmp_path.joinpath("pycodestyle.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + return importlib_metadata.PathDistribution(d) + + +@pytest.fixture +def flake8_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: flake8 +Version: 9001 +""" + entry_points = """\ +[console_scripts] +flake8 = flake8.main.cli:main + +[flake8.extension] +F = flake8.plugins.pyflakes:FlakesChecker +pycodestyle.bare_except = pycodestyle:bare_except +pycodestyle.blank_lines = pycodestyle:blank_lines + +[flake8.report] +default = flake8.formatting.default:Default +pylint = flake8.formatting.default:Pylint +""" + d = tmp_path.joinpath("flake8.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + d.joinpath("entry_points.txt").write_text(entry_points) + return importlib_metadata.PathDistribution(d) + + +@pytest.fixture +def flake8_foo_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: flake8-foo +Version: 1.2.3 +""" + eps = """\ +[console_scripts] +foo = flake8_foo:main +[flake8.extension] +Q = flake8_foo:Plugin +[flake8.report] +foo = flake8_foo:Formatter +""" + d = tmp_path.joinpath("flake8_foo.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + d.joinpath("entry_points.txt").write_text(eps) + return importlib_metadata.PathDistribution(d) + + +@pytest.fixture +def mock_distribution(pyflakes_dist, pycodestyle_dist): + dists = {"pyflakes": pyflakes_dist, "pycodestyle": pycodestyle_dist} + with mock.patch.object(importlib_metadata, "distribution", dists.get): + yield + + +def test_flake8_plugins(flake8_dist, mock_distribution): + """Ensure entrypoints for flake8 are parsed specially.""" + + eps = flake8_dist.entry_points + ret = set(finder._flake8_plugins(eps, "flake8", "9001")) + assert ret == { + finder.Plugin( + "pyflakes", + "9000.1.0", + importlib_metadata.EntryPoint( + "F", + "flake8.plugins.pyflakes:FlakesChecker", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib_metadata.EntryPoint( + "pycodestyle.bare_except", + "pycodestyle:bare_except", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib_metadata.EntryPoint( + "pycodestyle.blank_lines", + "pycodestyle:blank_lines", + "flake8.extension", + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib_metadata.EntryPoint( + "default", "flake8.formatting.default:Default", "flake8.report" + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib_metadata.EntryPoint( + "pylint", "flake8.formatting.default:Pylint", "flake8.report" + ), + ), + } + + +def test_importlib_plugins( + tmp_path, + flake8_dist, + flake8_foo_dist, + mock_distribution, + caplog, +): + """Ensure we can load plugins from importlib_metadata.""" + + # make sure flake8-colors is skipped + flake8_colors_metadata = """\ +Metadata-Version: 2.1 +Name: flake8-colors +Version: 1.2.3 +""" + flake8_colors_eps = """\ +[flake8.extension] +flake8-colors = flake8_colors:ColorFormatter +""" + flake8_colors_d = tmp_path.joinpath("flake8_colors.dist-info") + flake8_colors_d.mkdir() + flake8_colors_d.joinpath("METADATA").write_text(flake8_colors_metadata) + flake8_colors_d.joinpath("entry_points.txt").write_text(flake8_colors_eps) + flake8_colors_dist = importlib_metadata.PathDistribution(flake8_colors_d) + + unrelated_metadata = """\ +Metadata-Version: 2.1 +Name: unrelated +Version: 4.5.6 +""" + unrelated_eps = """\ +[console_scripts] +unrelated = unrelated:main +""" + unrelated_d = tmp_path.joinpath("unrelated.dist-info") + unrelated_d.mkdir() + unrelated_d.joinpath("METADATA").write_text(unrelated_metadata) + unrelated_d.joinpath("entry_points.txt").write_text(unrelated_eps) + unrelated_dist = importlib_metadata.PathDistribution(unrelated_d) + + with mock.patch.object( + importlib_metadata, + "distributions", + return_value=[ + flake8_dist, + flake8_colors_dist, + flake8_foo_dist, + unrelated_dist, + ], + ): + ret = set(finder._find_importlib_plugins()) + + assert ret == { + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib_metadata.EntryPoint( + "Q", "flake8_foo:Plugin", "flake8.extension" + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib_metadata.EntryPoint( + "pycodestyle.bare_except", + "pycodestyle:bare_except", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib_metadata.EntryPoint( + "pycodestyle.blank_lines", + "pycodestyle:blank_lines", + "flake8.extension", + ), + ), + finder.Plugin( + "pyflakes", + "9000.1.0", + importlib_metadata.EntryPoint( + "F", + "flake8.plugins.pyflakes:FlakesChecker", + "flake8.extension", + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib_metadata.EntryPoint( + "default", "flake8.formatting.default:Default", "flake8.report" + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib_metadata.EntryPoint( + "pylint", "flake8.formatting.default:Pylint", "flake8.report" + ), + ), + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib_metadata.EntryPoint( + "foo", "flake8_foo:Formatter", "flake8.report" + ), + ), + } + + assert caplog.record_tuples == [ + ( + "flake8.plugins.finder", + 30, + "flake8-colors plugin is obsolete in flake8>=4.1", + ), + ] + + +def test_find_local_plugins_nothing(): + cfg = configparser.RawConfigParser() + assert set(finder._find_local_plugins(cfg)) == set() + + +@pytest.fixture +def local_plugin_cfg(): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8:local-plugins") + cfg.set("flake8:local-plugins", "extension", "Y=mod2:attr, X = mod:attr") + cfg.set("flake8:local-plugins", "report", "Z=mod3:attr") + return cfg + + +def test_find_local_plugins(local_plugin_cfg): + ret = set(finder._find_local_plugins(local_plugin_cfg)) + assert ret == { + finder.Plugin( + "local", + "local", + importlib_metadata.EntryPoint( + "X", + "mod:attr", + "flake8.extension", + ), + ), + finder.Plugin( + "local", + "local", + importlib_metadata.EntryPoint( + "Y", + "mod2:attr", + "flake8.extension", + ), + ), + finder.Plugin( + "local", + "local", + importlib_metadata.EntryPoint( + "Z", + "mod3:attr", + "flake8.report", + ), + ), + } + + +def test_find_plugins( + tmp_path, + flake8_dist, + flake8_foo_dist, + mock_distribution, + local_plugin_cfg, +): + with mock.patch.object( + importlib_metadata, + "distributions", + return_value=[flake8_dist, flake8_foo_dist], + ): + ret = finder.find_plugins(local_plugin_cfg) + + assert ret == [ + finder.Plugin( + "flake8", + "9001", + importlib_metadata.EntryPoint( + "default", "flake8.formatting.default:Default", "flake8.report" + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib_metadata.EntryPoint( + "pylint", "flake8.formatting.default:Pylint", "flake8.report" + ), + ), + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib_metadata.EntryPoint( + "Q", "flake8_foo:Plugin", "flake8.extension" + ), + ), + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib_metadata.EntryPoint( + "foo", "flake8_foo:Formatter", "flake8.report" + ), + ), + finder.Plugin( + "local", + "local", + importlib_metadata.EntryPoint("X", "mod:attr", "flake8.extension"), + ), + finder.Plugin( + "local", + "local", + importlib_metadata.EntryPoint( + "Y", "mod2:attr", "flake8.extension" + ), + ), + finder.Plugin( + "local", + "local", + importlib_metadata.EntryPoint("Z", "mod3:attr", "flake8.report"), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib_metadata.EntryPoint( + "pycodestyle.bare_except", + "pycodestyle:bare_except", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib_metadata.EntryPoint( + "pycodestyle.blank_lines", + "pycodestyle:blank_lines", + "flake8.extension", + ), + ), + finder.Plugin( + "pyflakes", + "9000.1.0", + importlib_metadata.EntryPoint( + "F", + "flake8.plugins.pyflakes:FlakesChecker", + "flake8.extension", + ), + ), + ] + + +def test_find_local_plugin_paths_missing(tmp_path): + cfg = configparser.RawConfigParser() + assert finder.find_local_plugin_paths(cfg, str(tmp_path)) == [] + + +def test_find_local_plugin_paths(tmp_path): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8:local-plugins") + cfg.set("flake8:local-plugins", "paths", "./a, ./b") + ret = finder.find_local_plugin_paths(cfg, str(tmp_path)) + + assert ret == [str(tmp_path.joinpath("a")), str(tmp_path.joinpath("b"))] + + +def test_parameters_for_class_plugin(): + """Verify that we can retrieve the parameters for a class plugin.""" + + class FakeCheck: + def __init__(self, tree): + raise NotImplementedError + + assert finder._parameters_for(FakeCheck) == {"tree": True} + + +def test_parameters_for_function_plugin(): + """Verify that we retrieve the parameters for a function plugin.""" + + def fake_plugin(physical_line, self, tree, optional=None): + raise NotImplementedError + + assert finder._parameters_for(fake_plugin) == { + "physical_line": True, + "self": True, + "tree": True, + "optional": False, + } + + +def test_load_plugin_import_error(): + plugin = _plugin(ep=_ep(value="dne:dne")) + + with pytest.raises(FailedToLoadPlugin) as excinfo: + finder._load_plugin(plugin) + + pkg, e = excinfo.value.args + assert pkg == "local" + assert isinstance(e, ModuleNotFoundError) + + +def test_load_plugin_not_callable(): + plugin = _plugin(ep=_ep(value="os:curdir")) + + with pytest.raises(FailedToLoadPlugin) as excinfo: + finder._load_plugin(plugin) + + pkg, e = excinfo.value.args + assert pkg == "local" + assert isinstance(e, TypeError) + assert e.args == ("expected loaded plugin to be callable",) + + +def test_load_plugin_ok(): + plugin = _plugin(ep=_ep(value="flake8.plugins.pyflakes:FlakesChecker")) + + loaded = finder._load_plugin(plugin) + + assert loaded == finder.LoadedPlugin( + plugin, + FlakesChecker, + {"tree": True, "file_tokens": True, "filename": True}, + ) + + +@pytest.fixture +def reset_sys(): + orig_path = sys.path[:] + orig_modules = sys.modules.copy() + yield + sys.path[:] = orig_path + sys.modules.clear() + sys.modules.update(orig_modules) + + +@pytest.mark.usefixtures("reset_sys") +def test_import_plugins_extends_sys_path(): + plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) + + ret = finder._import_plugins([plugin], ["tests/integration/subdir"]) + + import aplugin + + assert ret == [ + finder.LoadedPlugin( + plugin, + aplugin.ExtensionTestPlugin2, + {"tree": True}, + ), + ] + + +def test_classify_plugins(): + report_plugin = _loaded( + plugin=_plugin(ep=_ep(name="R", group="flake8.report")) + ) + tree_plugin = _loaded(parameters={"tree": True}) + logical_line_plugin = _loaded(parameters={"logical_line": True}) + physical_line_plugin = _loaded(parameters={"physical_line": True}) + + classified = finder._classify_plugins( + [report_plugin, tree_plugin, logical_line_plugin, physical_line_plugin] + ) + + assert classified == finder.Plugins( + checkers=finder.Checkers( + tree=[tree_plugin], + logical_line=[logical_line_plugin], + physical_line=[physical_line_plugin], + ), + reporters={"R": report_plugin}, + ) + + +@pytest.mark.usefixtures("reset_sys") +def test_load_plugins(): + plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) + + ret = finder.load_plugins([plugin], ["tests/integration/subdir"]) + + import aplugin + + assert ret == finder.Plugins( + checkers=finder.Checkers( + tree=[ + finder.LoadedPlugin( + plugin, + aplugin.ExtensionTestPlugin2, + {"tree": True}, + ), + ], + logical_line=[], + physical_line=[], + ), + reporters={}, + ) diff --git a/tests/unit/plugins/reporter_test.py b/tests/unit/plugins/reporter_test.py new file mode 100644 index 0000000..4b46cc4 --- /dev/null +++ b/tests/unit/plugins/reporter_test.py @@ -0,0 +1,74 @@ +import argparse + +import pytest + +from flake8._compat import importlib_metadata +from flake8.formatting import default +from flake8.plugins import finder +from flake8.plugins import reporter + + +def _opts(**kwargs): + kwargs.setdefault("quiet", 0), + kwargs.setdefault("color", "never") + kwargs.setdefault("output_file", None) + return argparse.Namespace(**kwargs) + + +@pytest.fixture +def reporters(): + def _plugin(name, cls): + return finder.LoadedPlugin( + finder.Plugin( + "flake8", + "123", + importlib_metadata.EntryPoint( + name, f"{cls.__module__}:{cls.__name__}", "flake8.report" + ), + ), + cls, + {"options": True}, + ) + + return { + "default": _plugin("default", default.Default), + "pylint": _plugin("pylint", default.Pylint), + "quiet-filename": _plugin("quiet-filename", default.FilenameOnly), + "quiet-nothing": _plugin("quiet-nothing", default.Nothing), + } + + +def test_make_formatter_default(reporters): + ret = reporter.make(reporters, _opts(format="default")) + assert isinstance(ret, default.Default) + assert ret.error_format == default.Default.error_format + + +def test_make_formatter_quiet_filename(reporters): + ret = reporter.make(reporters, _opts(format="default", quiet=1)) + assert isinstance(ret, default.FilenameOnly) + + +@pytest.mark.parametrize("quiet", (2, 3)) +def test_make_formatter_very_quiet(reporters, quiet): + ret = reporter.make(reporters, _opts(format="default", quiet=quiet)) + assert isinstance(ret, default.Nothing) + + +def test_make_formatter_custom(reporters): + ret = reporter.make(reporters, _opts(format="pylint")) + assert isinstance(ret, default.Pylint) + + +def test_make_formatter_format_string(reporters, caplog): + ret = reporter.make(reporters, _opts(format="hi %(code)s")) + assert isinstance(ret, default.Default) + assert ret.error_format == "hi %(code)s" + + assert caplog.record_tuples == [ + ( + "flake8.plugins.reporter", + 30, + "'hi %(code)s' is an unknown formatter. Falling back to default.", + ) + ] diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 9f4b1e8..508f83b 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -1,7 +1,5 @@ """Tests for the Application class.""" import argparse -import sys -from unittest import mock import pytest @@ -44,62 +42,3 @@ def test_application_exit_code( application.options = options(exit_zero=exit_zero) assert application.exit_code() == value - - -def test_warns_on_unknown_formatter_plugin_name(application): - """Verify we log a warning with an unfound plugin.""" - default = mock.Mock() - execute = default.execute - application.formatting_plugins = { - "default": default, - } - with mock.patch.object(app.LOG, "warning") as warning: - assert execute is application.formatter_for("fake-plugin-name") - - assert warning.called is True - assert warning.call_count == 1 - - -def test_returns_specified_plugin(application): - """Verify we get the plugin we want.""" - desired = mock.Mock() - execute = desired.execute - application.formatting_plugins = { - "default": mock.Mock(), - "desired": desired, - } - - with mock.patch.object(app.LOG, "warning") as warning: - assert execute is application.formatter_for("desired") - - assert warning.called is False - - -def test_prelim_opts_args(application): - """Verify we get sensible prelim opts and args.""" - opts, args = application.parse_preliminary_options( - ["--foo", "--verbose", "src", "setup.py", "--statistics", "--version"] - ) - - assert opts.verbose - assert args == ["--foo", "src", "setup.py", "--statistics", "--version"] - - -def test_prelim_opts_ignore_help(application): - """Verify -h/--help is not handled.""" - # GIVEN - - # WHEN - _, args = application.parse_preliminary_options(["--help", "-h"]) - - # THEN - assert args == ["--help", "-h"] - - -def test_prelim_opts_handles_empty(application): - """Verify empty argv lists are handled correctly.""" - irrelevant_args = ["myexe", "/path/to/foo"] - with mock.patch.object(sys, "argv", irrelevant_args): - opts, args = application.parse_preliminary_options([]) - - assert args == [] diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 09a0f65..c6114f6 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -7,6 +7,7 @@ import pytest from flake8 import checker from flake8.main.options import JobsArgument +from flake8.plugins import finder def style_guide_mock(): @@ -22,7 +23,7 @@ def style_guide_mock(): def _parallel_checker_manager(): """Call Manager.run() and return the number of calls to `run_serial`.""" style_guide = style_guide_mock() - manager = checker.Manager(style_guide, []) + manager = checker.Manager(style_guide, finder.Checkers([], [], [])) # multiple checkers is needed for parallel mode manager.checkers = [mock.Mock(), mock.Mock()] return manager @@ -54,7 +55,7 @@ def test_oserrors_are_reraised(_): def test_multiprocessing_is_disabled(_): """Verify not being able to import multiprocessing forces jobs to 0.""" style_guide = style_guide_mock() - manager = checker.Manager(style_guide, []) + manager = checker.Manager(style_guide, finder.Checkers([], [], [])) assert manager.jobs == 0 @@ -68,7 +69,7 @@ def test_multiprocessing_cpu_count_not_implemented(): "cpu_count", side_effect=NotImplementedError, ): - manager = checker.Manager(style_guide, []) + manager = checker.Manager(style_guide, finder.Checkers([], [], [])) assert manager.jobs == 0 @@ -77,13 +78,7 @@ def test_make_checkers(_): """Verify that we create a list of FileChecker instances.""" style_guide = style_guide_mock() style_guide.options.filenames = ["file1", "file2"] - checkplugins = mock.Mock() - checkplugins.to_dictionary.return_value = { - "ast_plugins": [], - "logical_line_plugins": [], - "physical_line_plugins": [], - } - manager = checker.Manager(style_guide, checkplugins) + manager = checker.Manager(style_guide, finder.Checkers([], [], [])) with mock.patch("flake8.utils.fnmatch", return_value=True): with mock.patch("flake8.processor.FileProcessor"): diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index 28ab48e..284dec6 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -1,56 +1,47 @@ -"""Tests for our debugging module.""" from unittest import mock -import pytest - +from flake8._compat import importlib_metadata from flake8.main import debug +from flake8.plugins import finder -@pytest.mark.parametrize( - ("versions", "expected"), - ( - ([], []), - ( - [("p1", "1"), ("p2", "2"), ("p1", "1")], - [ - {"plugin": "p1", "version": "1"}, - {"plugin": "p2", "version": "2"}, +def test_debug_information(): + def _plugin(pkg, version, ep_name): + return finder.LoadedPlugin( + finder.Plugin( + pkg, + version, + importlib_metadata.EntryPoint( + ep_name, "dne:dne", "flake8.extension" + ), + ), + None, + {}, + ) + + plugins = finder.Plugins( + checkers=finder.Checkers( + tree=[ + _plugin("pkg1", "1.2.3", "X1"), + _plugin("pkg1", "1.2.3", "X2"), + _plugin("pkg2", "4.5.6", "X3"), ], + logical_line=[], + physical_line=[], ), - ), -) -def test_plugins_from(versions, expected): - """Test that we format plugins appropriately.""" - option_manager = mock.Mock(**{"manager.versions.return_value": versions}) - assert expected == debug.plugins_from(option_manager) + reporters={}, + ) - -@mock.patch("platform.python_implementation", return_value="CPython") -@mock.patch("platform.python_version", return_value="3.5.3") -@mock.patch("platform.system", return_value="Linux") -def test_information(system, pyversion, pyimpl): - """Verify that we return all the information we care about.""" - expected = { - "version": "3.1.0", + info = debug.information("9001", plugins) + assert info == { + "version": "9001", "plugins": [ - {"plugin": "mccabe", "version": "0.5.9"}, - {"plugin": "pycodestyle", "version": "2.0.0"}, + {"plugin": "pkg1", "version": "1.2.3"}, + {"plugin": "pkg2", "version": "4.5.6"}, ], "platform": { - "python_implementation": "CPython", - "python_version": "3.5.3", - "system": "Linux", + "python_implementation": mock.ANY, + "python_version": mock.ANY, + "system": mock.ANY, }, } - plugins = mock.Mock( - **{ - "manager.versions.return_value": [ - ("pycodestyle", "2.0.0"), - ("mccabe", "0.5.9"), - ] - } - ) - assert expected == debug.information("3.1.0", plugins) - pyimpl.assert_called_once_with() - pyversion.assert_called_once_with() - system.assert_called_once_with() diff --git a/tests/unit/test_file_checker.py b/tests/unit/test_file_checker.py index bcc8b32..ee4f745 100644 --- a/tests/unit/test_file_checker.py +++ b/tests/unit/test_file_checker.py @@ -1,26 +1,33 @@ """Unit tests for the FileChecker class.""" +import argparse from unittest import mock import pytest import flake8 from flake8 import checker +from flake8._compat import importlib_metadata +from flake8.plugins import finder @mock.patch("flake8.checker.FileChecker._make_processor", return_value=None) def test_repr(*args): """Verify we generate a correct repr.""" file_checker = checker.FileChecker( - "example.py", - checks={}, - options=object(), + filename="example.py", + plugins=finder.Checkers([], [], []), + options=argparse.Namespace(), ) assert repr(file_checker) == "FileChecker for example.py" def test_nonexistent_file(): """Verify that checking non-existent file results in an error.""" - c = checker.FileChecker("foobar.py", checks={}, options=object()) + c = checker.FileChecker( + filename="example.py", + plugins=finder.Checkers([], [], []), + options=argparse.Namespace(), + ) assert c.processor is None assert not c.should_process @@ -31,17 +38,21 @@ def test_nonexistent_file(): def test_raises_exception_on_failed_plugin(tmp_path, default_options): """Checks that a failing plugin results in PluginExecutionFailed.""" - foobar = tmp_path / "foobar.py" - foobar.write_text("I exist!") # Create temp file - plugin = { - "name": "failure", - "plugin_name": "failure", # Both are necessary - "parameters": dict(), - "plugin": mock.MagicMock(side_effect=ValueError), - } - """Verify a failing plugin results in an plugin error""" + fname = tmp_path.joinpath("t.py") + fname.touch() + plugin = finder.LoadedPlugin( + finder.Plugin( + "plugin-name", + "1.2.3", + importlib_metadata.EntryPoint("X", "dne:dne", "flake8.extension"), + ), + mock.Mock(side_effect=ValueError), + {}, + ) fchecker = checker.FileChecker( - str(foobar), checks=[], options=default_options + filename=str(fname), + plugins=finder.Checkers([], [], []), + options=default_options, ) with pytest.raises(flake8.exceptions.PluginExecutionFailed): fchecker.run_check(plugin) diff --git a/tests/unit/test_main_options.py b/tests/unit/test_main_options.py new file mode 100644 index 0000000..aea2071 --- /dev/null +++ b/tests/unit/test_main_options.py @@ -0,0 +1,17 @@ +from flake8.main import options + + +def test_stage1_arg_parser(): + stage1_parser = options.stage1_arg_parser() + opts, args = stage1_parser.parse_known_args( + ["--foo", "--verbose", "src", "setup.py", "--statistics", "--version"] + ) + + assert opts.verbose + assert args == ["--foo", "src", "setup.py", "--statistics", "--version"] + + +def test_stage1_arg_parser_ignores_help(): + stage1_parser = options.stage1_arg_parser() + _, args = stage1_parser.parse_known_args(["--help", "-h"]) + assert args == ["--help", "-h"] diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index c6006d3..a5e3d33 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -252,17 +252,6 @@ def test_optparse_normalize_help(optmanager, capsys): assert "default: bar" in output -def test_optmanager_group(optmanager, capsys): - """Test that group(...) causes options to be assigned to a group.""" - with optmanager.group("groupname"): - optmanager.add_option("--foo") - with pytest.raises(SystemExit): - optmanager.parse_args(["--help"]) - out, err = capsys.readouterr() - output = out + err - assert "\ngroupname:\n" in output - - @pytest.mark.parametrize( ("s", "is_auto", "n_jobs"), ( diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py deleted file mode 100644 index c41198e..0000000 --- a/tests/unit/test_plugin.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Tests for flake8.plugins.manager.Plugin.""" -import argparse -from unittest import mock - -import pytest - -from flake8 import exceptions -from flake8.options import manager as options_manager -from flake8.plugins import manager - - -def test_load_plugin_fallsback_on_old_setuptools(): - """Verify we fallback gracefully to on old versions of setuptools.""" - entry_point = mock.Mock(spec=["load"]) - plugin = manager.Plugin("T000", entry_point) - - plugin.load_plugin() - entry_point.load.assert_called_once_with() - - -def test_load_plugin_is_idempotent(): - """Verify we use the preferred methods on new versions of setuptools.""" - entry_point = mock.Mock(spec=["load"]) - plugin = manager.Plugin("T000", entry_point) - - plugin.load_plugin() - plugin.load_plugin() - plugin.load_plugin() - entry_point.load.assert_called_once_with() - - -def test_load_plugin_catches_and_reraises_exceptions(): - """Verify we raise our own FailedToLoadPlugin.""" - entry_point = mock.Mock(spec=["load"]) - entry_point.load.side_effect = ValueError("Test failure") - plugin = manager.Plugin("T000", entry_point) - - with pytest.raises(exceptions.FailedToLoadPlugin): - plugin.load_plugin() - - -def test_load_noncallable_plugin(): - """Verify that we do not load a non-callable plugin.""" - entry_point = mock.Mock(spec=["load"]) - entry_point.load.return_value = mock.NonCallableMock() - plugin = manager.Plugin("T000", entry_point) - - with pytest.raises(exceptions.FailedToLoadPlugin): - plugin.load_plugin() - entry_point.load.assert_called_once_with() - - -def test_plugin_property_loads_plugin_on_first_use(): - """Verify that we load our plugin when we first try to use it.""" - entry_point = mock.Mock(spec=["load"]) - plugin = manager.Plugin("T000", entry_point) - - assert plugin.plugin is not None - entry_point.load.assert_called_once_with() - - -def test_execute_calls_plugin_with_passed_arguments(): - """Verify that we pass arguments directly to the plugin.""" - entry_point = mock.Mock(spec=["load"]) - plugin_obj = mock.Mock() - plugin = manager.Plugin("T000", entry_point) - plugin._plugin = plugin_obj - - plugin.execute("arg1", "arg2", kwarg1="value1", kwarg2="value2") - plugin_obj.assert_called_once_with( - "arg1", "arg2", kwarg1="value1", kwarg2="value2" - ) - - # Extra assertions - assert entry_point.load.called is False - - -def test_version_proxies_to_the_plugin(): - """Verify that we pass arguments directly to the plugin.""" - entry_point = mock.Mock(spec=["load"]) - plugin_obj = mock.Mock(spec_set=["version"]) - plugin_obj.version = "a.b.c" - plugin = manager.Plugin("T000", entry_point) - plugin._plugin = plugin_obj - - assert plugin.version == "a.b.c" - - -def test_register_options(): - """Verify we call add_options on the plugin only if it exists.""" - # Set up our mocks and Plugin object - entry_point = mock.Mock(spec=["load"]) - plugin_obj = mock.Mock( - spec_set=["name", "version", "add_options", "parse_options"] - ) - option_manager = mock.MagicMock(spec=options_manager.OptionManager) - plugin = manager.Plugin("T000", entry_point) - plugin._plugin = plugin_obj - - # Call the method we're testing. - plugin.register_options(option_manager) - - # Assert that we call add_options - plugin_obj.add_options.assert_called_once_with(option_manager) - - -def test_register_options_checks_plugin_for_method(): - """Verify we call add_options on the plugin only if it exists.""" - # Set up our mocks and Plugin object - entry_point = mock.Mock(spec=["load"]) - plugin_obj = mock.Mock(spec_set=["name", "version", "parse_options"]) - option_manager = mock.Mock(spec=["register_plugin"]) - plugin = manager.Plugin("T000", entry_point) - plugin._plugin = plugin_obj - - # Call the method we're testing. - plugin.register_options(option_manager) - - # Assert that we register the plugin - assert option_manager.register_plugin.called is False - - -def test_provide_options(): - """Verify we call add_options on the plugin only if it exists.""" - # Set up our mocks and Plugin object - entry_point = mock.Mock(spec=["load"]) - plugin_obj = mock.Mock( - spec_set=["name", "version", "add_options", "parse_options"] - ) - option_values = argparse.Namespace(enable_extensions=[]) - option_manager = mock.Mock() - plugin = manager.Plugin("T000", entry_point) - plugin._plugin = plugin_obj - - # Call the method we're testing. - plugin.provide_options(option_manager, option_values, None) - - # Assert that we call add_options - plugin_obj.parse_options.assert_called_once_with( - option_manager, option_values, None - ) - - -@pytest.mark.parametrize( - "ignore_list, code, expected_list", - [ - (["E", "W", "F", "C9"], "W", ["E", "F", "C9"]), - (["E", "W", "F"], "C9", ["E", "W", "F"]), - ], -) -def test_enable(ignore_list, code, expected_list): - """Verify that enabling a plugin removes it from the ignore list.""" - options = mock.Mock(ignore=ignore_list) - optmanager = mock.Mock() - plugin = manager.Plugin(code, mock.Mock()) - - plugin.enable(optmanager, options) - - assert options.ignore == expected_list - - -def test_enable_without_providing_parsed_options(): - """Verify that enabling a plugin removes it from the ignore list.""" - optmanager = mock.Mock() - plugin = manager.Plugin("U4", mock.Mock()) - - plugin.enable(optmanager) - - optmanager.remove_from_default_ignore.assert_called_once_with(["U4"]) diff --git a/tests/unit/test_plugin_manager.py b/tests/unit/test_plugin_manager.py deleted file mode 100644 index 5a38a38..0000000 --- a/tests/unit/test_plugin_manager.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Tests for flake8.plugins.manager.PluginManager.""" -from unittest import mock - -from flake8._compat import importlib_metadata -from flake8.plugins import manager - - -@mock.patch.object(importlib_metadata, "entry_points") -def test_calls_entrypoints_on_instantiation(entry_points_mck): - """Verify that we call entry_points() when we create a manager.""" - entry_points_mck.return_value = {} - manager.PluginManager(namespace="testing.entrypoints") - entry_points_mck.assert_called_once_with() - - -@mock.patch.object(importlib_metadata, "entry_points") -def test_calls_entrypoints_creates_plugins_automaticaly(entry_points_mck): - """Verify that we create Plugins on instantiation.""" - entry_points_mck.return_value = { - "testing.entrypoints": [ - importlib_metadata.EntryPoint("T100", "", "testing.entrypoints"), - importlib_metadata.EntryPoint("T200", "", "testing.entrypoints"), - ], - } - plugin_mgr = manager.PluginManager(namespace="testing.entrypoints") - - entry_points_mck.assert_called_once_with() - assert "T100" in plugin_mgr.plugins - assert "T200" in plugin_mgr.plugins - assert isinstance(plugin_mgr.plugins["T100"], manager.Plugin) - assert isinstance(plugin_mgr.plugins["T200"], manager.Plugin) - - -@mock.patch.object(importlib_metadata, "entry_points") -def test_handles_mapping_functions_across_plugins(entry_points_mck): - """Verify we can use the PluginManager call functions on all plugins.""" - entry_points_mck.return_value = { - "testing.entrypoints": [ - importlib_metadata.EntryPoint("T100", "", "testing.entrypoints"), - importlib_metadata.EntryPoint("T200", "", "testing.entrypoints"), - ], - } - plugin_mgr = manager.PluginManager(namespace="testing.entrypoints") - plugins = [plugin_mgr.plugins[name] for name in plugin_mgr.names] - - assert list(plugin_mgr.map(lambda x: x)) == plugins - - -@mock.patch.object(importlib_metadata, "entry_points") -def test_local_plugins(entry_points_mck): - """Verify PluginManager can load given local plugins.""" - entry_points_mck.return_value = {} - plugin_mgr = manager.PluginManager( - namespace="testing.entrypoints", local_plugins=["X = path.to:Plugin"] - ) - - assert plugin_mgr.plugins["X"].entry_point.value == "path.to:Plugin" diff --git a/tests/unit/test_plugin_type_manager.py b/tests/unit/test_plugin_type_manager.py deleted file mode 100644 index ed4fa8c..0000000 --- a/tests/unit/test_plugin_type_manager.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Tests for flake8.plugins.manager.PluginTypeManager.""" -from unittest import mock - -import pytest - -from flake8 import exceptions -from flake8.plugins import manager - -TEST_NAMESPACE = "testing.plugin-type-manager" - - -def create_plugin_mock(raise_exception=False): - """Create an auto-spec'd mock of a flake8 Plugin.""" - plugin = mock.create_autospec(manager.Plugin, instance=True) - if raise_exception: - plugin.load_plugin.side_effect = exceptions.FailedToLoadPlugin( - plugin_name="T101", - exception=ValueError("Test failure"), - ) - return plugin - - -def create_mapping_manager_mock(plugins): - """Create a mock for the PluginManager.""" - # Have a function that will actually call the method underneath - def fake_map(func): - for plugin in plugins.values(): - yield func(plugin) - - # Mock out the PluginManager instance - manager_mock = mock.Mock(spec=["map"]) - # Replace the map method - manager_mock.map = fake_map - # Store the plugins - manager_mock.plugins = plugins - return manager_mock - - -class FakeTestType(manager.PluginTypeManager): - """Fake PluginTypeManager.""" - - namespace = TEST_NAMESPACE - - -@mock.patch("flake8.plugins.manager.PluginManager", autospec=True) -def test_instantiates_a_manager(PluginManager): # noqa: N803 - """Verify we create a PluginManager on instantiation.""" - FakeTestType() - - PluginManager.assert_called_once_with(TEST_NAMESPACE, local_plugins=None) - - -@mock.patch("flake8.plugins.manager.PluginManager", autospec=True) -def test_proxies_names_to_manager(PluginManager): # noqa: N803 - """Verify we proxy the names attribute.""" - PluginManager.return_value = mock.Mock(names=["T100", "T200", "T300"]) - type_mgr = FakeTestType() - - assert type_mgr.names == ["T100", "T200", "T300"] - - -@mock.patch("flake8.plugins.manager.PluginManager", autospec=True) -def test_proxies_plugins_to_manager(PluginManager): # noqa: N803 - """Verify we proxy the plugins attribute.""" - PluginManager.return_value = mock.Mock(plugins=["T100", "T200", "T300"]) - type_mgr = FakeTestType() - - assert type_mgr.plugins == ["T100", "T200", "T300"] - - -def test_generate_call_function(): - """Verify the function we generate.""" - optmanager = object() - plugin = mock.Mock(method_name=lambda x: x) - func = manager.PluginTypeManager._generate_call_function( - "method_name", - optmanager, - ) - - assert callable(func) - assert func(plugin) is optmanager - - -@mock.patch("flake8.plugins.manager.PluginManager", autospec=True) -def test_load_plugins(PluginManager): # noqa: N803 - """Verify load plugins loads *every* plugin.""" - # Create a bunch of fake plugins - plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} - # Return our PluginManager mock - PluginManager.return_value.plugins = plugins - - type_mgr = FakeTestType() - # Load the plugins (do what we're actually testing) - type_mgr.load_plugins() - # Assert that our closure does what we think it does - for plugin in plugins.values(): - plugin.load_plugin.assert_called_once_with() - assert type_mgr.plugins_loaded is True - - -@mock.patch("flake8.plugins.manager.PluginManager") -def test_load_plugins_fails(PluginManager): # noqa: N803 - """Verify load plugins bubbles up exceptions.""" - plugins_list = [create_plugin_mock(i == 1) for i in range(8)] - plugins = {"T10%i" % i: plugin for i, plugin in enumerate(plugins_list)} - # Return our PluginManager mock - PluginManager.return_value.plugins = plugins - - type_mgr = FakeTestType() - with pytest.raises(exceptions.FailedToLoadPlugin): - type_mgr.load_plugins() - - # Assert we didn't finish loading plugins - assert type_mgr.plugins_loaded is False - # Assert the first two plugins had their load_plugin method called - plugins_list[0].load_plugin.assert_called_once_with() - plugins_list[1].load_plugin.assert_called_once_with() - # Assert the rest of the plugins were not loaded - for plugin in plugins_list[2:]: - assert plugin.load_plugin.called is False - - -@mock.patch("flake8.plugins.manager.PluginManager") -def test_register_options(PluginManager): # noqa: N803 - """Test that we map over every plugin to register options.""" - plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} - # Return our PluginManager mock - PluginManager.return_value = create_mapping_manager_mock(plugins) - optmanager = object() - - type_mgr = FakeTestType() - type_mgr.register_options(optmanager) - - for plugin in plugins.values(): - plugin.register_options.assert_called_with(optmanager) - - -@mock.patch("flake8.plugins.manager.PluginManager") -def test_provide_options(PluginManager): # noqa: N803 - """Test that we map over every plugin to provide parsed options.""" - plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} - # Return our PluginManager mock - PluginManager.return_value = create_mapping_manager_mock(plugins) - optmanager = object() - options = object() - - type_mgr = FakeTestType() - type_mgr.provide_options(optmanager, options, []) - - for plugin in plugins.values(): - plugin.provide_options.assert_called_with(optmanager, options, []) - - -@mock.patch("flake8.plugins.manager.PluginManager", autospec=True) -def test_proxy_contains_to_managers_plugins_dict(PluginManager): # noqa: N803 - """Verify that we proxy __contains__ to the manager's dictionary.""" - plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} - # Return our PluginManager mock - PluginManager.return_value.plugins = plugins - - type_mgr = FakeTestType() - for i in range(8): - key = "T10%i" % i - assert key in type_mgr - - -@mock.patch("flake8.plugins.manager.PluginManager") -def test_proxies_getitem_to_managers_plugins_dict(PluginManager): # noqa: N803 - """Verify that we can use the PluginTypeManager like a dictionary.""" - plugins = {"T10%i" % i: create_plugin_mock() for i in range(8)} - # Return our PluginManager mock - PluginManager.return_value.plugins = plugins - - type_mgr = FakeTestType() - for i in range(8): - key = "T10%i" % i - assert type_mgr[key] is plugins[key] diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index eb76572..167ba72 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -9,7 +9,6 @@ import pytest from flake8 import exceptions from flake8 import utils -from flake8.plugins import manager as plugin_manager RELATIVE_PATHS = ["flake8", "pep8", "pyflakes", "mccabe"] @@ -181,34 +180,6 @@ def test_fnmatch(filename, patterns, expected): assert utils.fnmatch(filename, patterns) is expected -def test_parameters_for_class_plugin(): - """Verify that we can retrieve the parameters for a class plugin.""" - - class FakeCheck: - def __init__(self, tree): - raise NotImplementedError - - plugin = plugin_manager.Plugin("plugin-name", object()) - plugin._plugin = FakeCheck - assert utils.parameters_for(plugin) == {"tree": True} - - -def test_parameters_for_function_plugin(): - """Verify that we retrieve the parameters for a function plugin.""" - - def fake_plugin(physical_line, self, tree, optional=None): - raise NotImplementedError - - plugin = plugin_manager.Plugin("plugin-name", object()) - plugin._plugin = fake_plugin - assert utils.parameters_for(plugin) == { - "physical_line": True, - "self": True, - "tree": True, - "optional": False, - } - - def read_diff_file(filename): """Read the diff file in its entirety.""" with open(filename) as fd: From a8333e2bf22dbbf22739a554a70e094d2c80f460 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jan 2022 18:28:11 -0500 Subject: [PATCH 050/257] move managing of off_by_default / enable_extensions to plugin loading --- src/flake8/api/legacy.py | 2 +- src/flake8/main/application.py | 8 ++- src/flake8/main/options.py | 18 +++--- src/flake8/options/manager.py | 6 +- src/flake8/plugins/finder.py | 43 +++++++++++-- src/flake8/style_guide.py | 7 +- tests/integration/test_checker.py | 2 +- tests/integration/test_plugins.py | 5 +- tests/unit/plugins/finder_test.py | 53 ++++++++++++++- tests/unit/test_debug.py | 1 + tests/unit/test_decision_engine.py | 100 +++++++---------------------- tests/unit/test_legacy_api.py | 3 +- 12 files changed, 137 insertions(+), 111 deletions(-) diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 0d9875f..0d848fc 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -38,7 +38,7 @@ def get_style_guide(**kwargs): isolated=prelim_opts.isolated, ) - application.find_plugins(cfg, cfg_dir) + application.find_plugins(cfg, cfg_dir, prelim_opts.enable_extensions) application.register_plugin_options() application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) # We basically want application.initialize to be called but with these diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 86fd89f..4b91145 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -119,14 +119,16 @@ class Application: self, cfg: configparser.RawConfigParser, cfg_dir: str, + enable_extensions: Optional[str], ) -> None: """Find and load the plugins for this application. Set :attr:`plugins` based on loaded plugins. """ - raw_plugins = finder.find_plugins(cfg) + raw = finder.find_plugins(cfg) local_plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - self.plugins = finder.load_plugins(raw_plugins, local_plugin_paths) + enabled = finder.parse_enabled(cfg, enable_extensions) + self.plugins = finder.load_plugins(raw, local_plugin_paths, enabled) def register_plugin_options(self) -> None: """Register options provided by plugins to our option manager.""" @@ -302,7 +304,7 @@ class Application: isolated=prelim_opts.isolated, ) - self.find_plugins(cfg, cfg_dir) + self.find_plugins(cfg, cfg_dir, prelim_opts.enable_extensions) self.register_plugin_options() self.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) self.make_formatter() diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 9c23d64..93178ac 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -15,6 +15,7 @@ def stage1_arg_parser() -> argparse.ArgumentParser: - ``--append-config`` - ``--config`` - ``--isolated`` + - ``--enable-extensions`` """ parser = argparse.ArgumentParser(add_help=False) @@ -59,6 +60,14 @@ def stage1_arg_parser() -> argparse.ArgumentParser: help="Ignore all configuration files.", ) + # Plugin enablement options + + parser.add_argument( + "--enable-extensions", + help="Enable plugins and extensions that are otherwise disabled " + "by default", + ) + return parser @@ -116,7 +125,6 @@ def register_default_options(option_manager: OptionManager) -> None: - ``--disable-noqa`` - ``--show-source`` - ``--statistics`` - - ``--enable-extensions`` - ``--exit-zero`` - ``-j``/``--jobs`` - ``--tee`` @@ -331,14 +339,6 @@ def register_default_options(option_manager: OptionManager) -> None: ) # Flake8 options - add_option( - "--enable-extensions", - default="", - parse_from_config=True, - comma_separated_list=True, - help="Enable plugins and extensions that are otherwise disabled " - "by default", - ) add_option( "--exit-zero", diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index d448e1b..d3f6ec7 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -376,11 +376,7 @@ class OptionManager: _set_group(loaded.plugin.package) add_options(self) - # if the plugin is off by default, disable it! - if getattr(loaded.obj, "off_by_default", False): - self.extend_default_ignore(loaded.entry_name) - else: - self.extend_default_select(loaded.entry_name) + self.extend_default_select(loaded.entry_name) # isn't strictly necessary, but seems cleaner self._current_group = None diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 4609474..fcd6aff 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -9,6 +9,8 @@ from typing import Generator from typing import Iterable from typing import List from typing import NamedTuple +from typing import Optional +from typing import Set from flake8 import utils from flake8._compat import importlib_metadata @@ -63,6 +65,7 @@ class Plugins(NamedTuple): checkers: Checkers reporters: Dict[str, LoadedPlugin] + disabled: List[LoadedPlugin] def all_plugins(self) -> Generator[LoadedPlugin, None, None]: """Return an iterator over all :class:`LoadedPlugin`s.""" @@ -171,6 +174,24 @@ def find_local_plugin_paths( return utils.normalize_paths(paths, cfg_dir) +def parse_enabled( + cfg: configparser.RawConfigParser, + enable_extensions: Optional[str], +) -> Set[str]: + """Parse --enable-extensions.""" + if enable_extensions is not None: + return set(utils.parse_comma_separated_list(enable_extensions)) + else: + # ideally this would reuse our config parsing framework but we need to + # parse this from preliminary options before plugins are enabled + for opt in ("enable_extensions", "enable-extensions"): + val = cfg.get("flake8", opt, fallback=None) + if val is not None: + return set(utils.parse_comma_separated_list(val)) + else: + return set() + + def _parameters_for(func: Any) -> Dict[str, bool]: """Return the parameters for the plugin. @@ -218,14 +239,23 @@ def _import_plugins( return [_load_plugin(p) for p in plugins] -def _classify_plugins(plugins: List[LoadedPlugin]) -> Plugins: +def _classify_plugins( + plugins: List[LoadedPlugin], + enabled: Set[str], +) -> Plugins: tree = [] logical_line = [] physical_line = [] reporters = {} + disabled = [] for loaded in plugins: - if loaded.plugin.entry_point.group == "flake8.report": + if ( + getattr(loaded.obj, "off_by_default", False) + and loaded.plugin.entry_point.name not in enabled + ): + disabled.append(loaded) + elif loaded.plugin.entry_point.group == "flake8.report": reporters[loaded.entry_name] = loaded elif "tree" in loaded.parameters: tree.append(loaded) @@ -243,14 +273,19 @@ def _classify_plugins(plugins: List[LoadedPlugin]) -> Plugins: physical_line=physical_line, ), reporters=reporters, + disabled=disabled, ) -def load_plugins(plugins: List[Plugin], paths: List[str]) -> Plugins: +def load_plugins( + plugins: List[Plugin], + paths: List[str], + enabled: Set[str], +) -> Plugins: """Load and classify all flake8 plugins. - first: extends ``sys.path`` with ``paths`` (to import local plugins) - next: converts the ``Plugin``s to ``LoadedPlugins`` - finally: classifies plugins into their specific types """ - return _classify_plugins(_import_plugins(plugins, paths)) + return _classify_plugins(_import_plugins(plugins, paths), enabled) diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index aca743a..c471fa6 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -160,14 +160,9 @@ class DecisionEngine: self.extended_selected = tuple( sorted(options.extended_default_select, reverse=True) ) - self.enabled_extensions = tuple(options.enable_extensions) self.all_selected = tuple( sorted( - itertools.chain( - self.selected, - options.extend_select, - self.enabled_extensions, - ), + itertools.chain(self.selected, options.extend_select), reverse=True, ) ) diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 96b7d4b..9583179 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -90,7 +90,7 @@ def mock_file_checker_with_plugin(plugin_target): ), ), ] - plugins = finder.load_plugins(to_load, []) + plugins = finder.load_plugins(to_load, [], set()) # Prevent it from reading lines from stdin or somewhere else with mock.patch( diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 682360c..e2c87e4 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -55,7 +55,7 @@ def test_enable_local_plugin_from_config(local_config): cfg, cfg_dir = config.load_config(local_config, [], isolated=False) plugins = finder.find_plugins(cfg) plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - loaded_plugins = finder.load_plugins(plugins, plugin_paths) + loaded_plugins = finder.load_plugins(plugins, plugin_paths, set()) (custom_extension,) = ( loaded @@ -82,7 +82,8 @@ def test_local_plugin_can_add_option(local_config): plugins = finder.find_plugins(cfg) plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - loaded_plugins = finder.load_plugins(plugins, plugin_paths) + enabled = finder.parse_enabled(cfg, stage1_args.enable_extensions) + loaded_plugins = finder.load_plugins(plugins, plugin_paths, enabled) option_manager = OptionManager( version="123", diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 993c13c..f41aba4 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -49,6 +49,7 @@ def test_plugins_all_plugins(): physical_line=[physical_line_plugin], ), reporters={"R": report_plugin}, + disabled=[], ) assert tuple(plugins.all_plugins()) == ( @@ -72,6 +73,7 @@ def test_plugins_versions_str(): # ignore local plugins "custom": _loaded(_plugin(package="local")), }, + disabled=[], ) assert plugins.versions_str() == "pkg1: 1, pkg2: 2" @@ -374,6 +376,25 @@ def test_find_local_plugins(local_plugin_cfg): } +def test_parse_enabled_not_specified(): + assert finder.parse_enabled(configparser.RawConfigParser(), None) == set() + + +def test_parse_enabled_from_commandline(): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "enable_extensions", "A,B,C") + assert finder.parse_enabled(cfg, "D,E,F") == {"D", "E", "F"} + + +@pytest.mark.parametrize("opt", ("enable_extensions", "enable-extensions")) +def test_parse_enabled_from_config(opt): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", opt, "A,B,C") + assert finder.parse_enabled(cfg, None) == {"A", "B", "C"} + + def test_find_plugins( tmp_path, flake8_dist, @@ -573,7 +594,13 @@ def test_classify_plugins(): physical_line_plugin = _loaded(parameters={"physical_line": True}) classified = finder._classify_plugins( - [report_plugin, tree_plugin, logical_line_plugin, physical_line_plugin] + [ + report_plugin, + tree_plugin, + logical_line_plugin, + physical_line_plugin, + ], + set(), ) assert classified == finder.Plugins( @@ -583,6 +610,27 @@ def test_classify_plugins(): physical_line=[physical_line_plugin], ), reporters={"R": report_plugin}, + disabled=[], + ) + + +def test_classify_plugins_enable_a_disabled_plugin(): + obj = mock.Mock(off_by_default=True) + plugin = _plugin(ep=_ep(name="ABC")) + loaded = _loaded(plugin=plugin, parameters={"tree": True}, obj=obj) + + classified_normal = finder._classify_plugins([loaded], set()) + classified_enabled = finder._classify_plugins([loaded], {"ABC"}) + + assert classified_normal == finder.Plugins( + checkers=finder.Checkers([], [], []), + reporters={}, + disabled=[loaded], + ) + assert classified_enabled == finder.Plugins( + checkers=finder.Checkers([loaded], [], []), + reporters={}, + disabled=[], ) @@ -590,7 +638,7 @@ def test_classify_plugins(): def test_load_plugins(): plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) - ret = finder.load_plugins([plugin], ["tests/integration/subdir"]) + ret = finder.load_plugins([plugin], ["tests/integration/subdir"], set()) import aplugin @@ -607,4 +655,5 @@ def test_load_plugins(): physical_line=[], ), reporters={}, + disabled=[], ) diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index 284dec6..1fc93ef 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -30,6 +30,7 @@ def test_debug_information(): physical_line=[], ), reporters={}, + disabled=[], ) info = debug.information("9001", plugins) diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index 213dd84..19d574a 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -16,7 +16,6 @@ def create_options(**kwargs): kwargs.setdefault("ignore", []) kwargs.setdefault("extend_ignore", []) kwargs.setdefault("disable_noqa", False) - kwargs.setdefault("enable_extensions", []) return argparse.Namespace(**kwargs) @@ -64,30 +63,25 @@ def test_was_ignored_implicitly_selects_errors( @pytest.mark.parametrize( - "select_list,extend_select,enable_extensions,error_code", - [ - (["E111", "E121"], [], [], "E111"), - (["E111", "E121"], [], [], "E121"), - (["E11", "E12"], [], [], "E121"), - (["E2", "E12"], [], [], "E121"), - (["E2", "E12"], [], [], "E211"), - (["E1"], ["E2"], [], "E211"), - (["E1"], [], ["E2"], "E211"), - ([], ["E2"], [], "E211"), - ([], [], ["E2"], "E211"), - (["E1"], ["E2"], [], "E211"), - (["E111"], ["E121"], ["E2"], "E121"), - ], + ("select_list", "extend_select", "error_code"), + ( + (["E111", "E121"], [], "E111"), + (["E111", "E121"], [], "E121"), + (["E11", "E12"], [], "E121"), + (["E2", "E12"], [], "E121"), + (["E2", "E12"], [], "E211"), + (["E1"], ["E2"], "E211"), + ([], ["E2"], "E211"), + (["E1"], ["E2"], "E211"), + (["E111"], ["E121"], "E121"), + ), ) -def test_was_selected_selects_errors( - select_list, extend_select, enable_extensions, error_code -): +def test_was_selected_selects_errors(select_list, extend_select, error_code): """Verify we detect users explicitly selecting an error.""" decider = style_guide.DecisionEngine( options=create_options( select=select_list, extend_select=extend_select, - enable_extensions=enable_extensions, ), ) @@ -199,15 +193,20 @@ def test_decision_for( @pytest.mark.parametrize( - "select,ignore,extended_default_ignore,extended_default_select," - "enabled_extensions,error_code,expected", + ( + "select", + "ignore", + "extended_default_ignore", + "extended_default_select", + "error_code", + "expected", + ), [ ( defaults.SELECT, [], [], ["I1"], - [], "I100", style_guide.Decision.Selected, ), @@ -216,7 +215,6 @@ def test_decision_for( [], [], ["I1"], - [], "I201", style_guide.Decision.Ignored, ), @@ -225,7 +223,6 @@ def test_decision_for( ["I2"], [], ["I1"], - [], "I101", style_guide.Decision.Selected, ), @@ -234,7 +231,6 @@ def test_decision_for( ["I2"], [], ["I1"], - [], "I201", style_guide.Decision.Ignored, ), @@ -243,7 +239,6 @@ def test_decision_for( ["I1"], [], ["I10"], - [], "I101", style_guide.Decision.Selected, ), @@ -252,43 +247,22 @@ def test_decision_for( ["I10"], [], ["I1"], - [], "I101", style_guide.Decision.Ignored, ), - ( - defaults.SELECT, - [], - [], - [], - ["U4"], - "U401", - style_guide.Decision.Selected, - ), ( defaults.SELECT, ["U401"], [], [], - ["U4"], "U401", style_guide.Decision.Ignored, ), - ( - defaults.SELECT, - ["U401"], - [], - [], - ["U4"], - "U402", - style_guide.Decision.Selected, - ), ( ["E", "W"], ["E13"], [], [], - [], "E131", style_guide.Decision.Ignored, ), @@ -297,18 +271,16 @@ def test_decision_for( ["E13"], [], [], - [], "E126", style_guide.Decision.Selected, ), - (["E2"], ["E21"], [], [], [], "E221", style_guide.Decision.Selected), - (["E2"], ["E21"], [], [], [], "E212", style_guide.Decision.Ignored), + (["E2"], ["E21"], [], [], "E221", style_guide.Decision.Selected), + (["E2"], ["E21"], [], [], "E212", style_guide.Decision.Ignored), ( ["F", "W"], ["C90"], [], ["I1"], - [], "C901", style_guide.Decision.Ignored, ), @@ -317,25 +289,14 @@ def test_decision_for( ["C"], [], [], - [], "E131", style_guide.Decision.Selected, ), - ( - defaults.SELECT, - defaults.IGNORE, - [], - [], - ["I"], - "I101", - style_guide.Decision.Selected, - ), ( defaults.SELECT, defaults.IGNORE, [], ["G"], - ["I"], "G101", style_guide.Decision.Selected, ), @@ -344,25 +305,14 @@ def test_decision_for( ["G1"], [], ["G"], - ["I"], "G101", style_guide.Decision.Ignored, ), - ( - defaults.SELECT, - ["E126"], - [], - [], - ["I"], - "I101", - style_guide.Decision.Selected, - ), ( ["E", "W"], defaults.IGNORE, [], ["I"], - [], "I101", style_guide.Decision.Ignored, ), @@ -371,7 +321,6 @@ def test_decision_for( defaults.IGNORE + ("I101",), ["I101"], [], - [], "I101", style_guide.Decision.Selected, ), @@ -380,7 +329,6 @@ def test_decision_for( defaults.IGNORE + ("I101",), ["I101"], [], - [], "I101", style_guide.Decision.Ignored, ), @@ -393,7 +341,6 @@ def test_more_specific_decision_for_logic( ignore, extended_default_ignore, extended_default_select, - enabled_extensions, error_code, expected, ): @@ -404,7 +351,6 @@ def test_more_specific_decision_for_logic( ignore=ignore, extended_default_select=extended_default_select, extended_default_ignore=extended_default_ignore, - enable_extensions=enabled_extensions, ), ) diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 1033f5e..6424f60 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -19,6 +19,7 @@ def test_get_style_guide(): isolated=False, output_file=None, verbose=0, + enable_extensions=None, ) mockedapp = mock.Mock() mockedapp.parse_preliminary_options.return_value = (prelim_opts, []) @@ -34,7 +35,7 @@ def test_get_style_guide(): application.assert_called_once_with() mockedapp.parse_preliminary_options.assert_called_once_with([]) - mockedapp.find_plugins.assert_called_once_with(cfg, cfg_dir) + mockedapp.find_plugins.assert_called_once_with(cfg, cfg_dir, None) mockedapp.register_plugin_options.assert_called_once_with() mockedapp.parse_configuration_and_cli.assert_called_once_with( cfg, cfg_dir, [] From b62edd334a4386e45aff52ad53a17c701dd89382 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jan 2022 19:26:43 -0500 Subject: [PATCH 051/257] fix extended_default_select from plugin loading --- src/flake8/options/manager.py | 3 ++- src/flake8/plugins/finder.py | 5 +++++ tests/integration/test_plugins.py | 1 + tests/unit/plugins/finder_test.py | 12 ++++++------ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index d3f6ec7..ad5543a 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -376,7 +376,8 @@ class OptionManager: _set_group(loaded.plugin.package) add_options(self) - self.extend_default_select(loaded.entry_name) + if loaded.plugin.entry_point.group == "flake8.extension": + self.extend_default_select([loaded.entry_name]) # isn't strictly necessary, but seems cleaner self._current_group = None diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index fcd6aff..6df0ccc 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -102,6 +102,11 @@ def _flake8_plugins( if ep.name == "F": yield Plugin(pyflakes_meta["name"], pyflakes_meta["version"], ep) elif ep.name.startswith("pycodestyle"): + # pycodestyle provides both `E` and `W` -- but our default select + # handles those + # ideally pycodestyle's plugin entrypoints would exactly represent + # the codes they produce... + ep = importlib_metadata.EntryPoint("E", ep.value, ep.group) yield Plugin( pycodestyle_meta["name"], pycodestyle_meta["version"], ep ) diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index e2c87e4..e7f2d38 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -95,6 +95,7 @@ def test_local_plugin_can_add_option(local_config): args = aggregator.aggregate_options(option_manager, cfg, cfg_dir, argv) + assert args.extended_default_select == {"XE", "F", "E", "C90"} assert args.anopt == "foo" diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index f41aba4..0e5f81f 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -179,7 +179,7 @@ def test_flake8_plugins(flake8_dist, mock_distribution): "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "pycodestyle.bare_except", + "E", "pycodestyle:bare_except", "flake8.extension", ), @@ -188,7 +188,7 @@ def test_flake8_plugins(flake8_dist, mock_distribution): "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "pycodestyle.blank_lines", + "E", "pycodestyle:blank_lines", "flake8.extension", ), @@ -274,7 +274,7 @@ unrelated = unrelated:main "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "pycodestyle.bare_except", + "E", "pycodestyle:bare_except", "flake8.extension", ), @@ -283,7 +283,7 @@ unrelated = unrelated:main "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "pycodestyle.blank_lines", + "E", "pycodestyle:blank_lines", "flake8.extension", ), @@ -459,7 +459,7 @@ def test_find_plugins( "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "pycodestyle.bare_except", + "E", "pycodestyle:bare_except", "flake8.extension", ), @@ -468,7 +468,7 @@ def test_find_plugins( "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "pycodestyle.blank_lines", + "E", "pycodestyle:blank_lines", "flake8.extension", ), From 4d15e0c13627da613fe3e78e155331008cbededc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 22:11:04 +0000 Subject: [PATCH 052/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ec1266..81622e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.31.0 hooks: - id: pyupgrade args: [--py36-plus] From 4cb1dc8c44ad2e891f1b8c8c7d774353311e7b06 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 11:44:31 -0500 Subject: [PATCH 053/257] perform path normalization on '.' --- src/flake8/utils.py | 8 +++++--- tests/unit/test_utils.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/flake8/utils.py b/src/flake8/utils.py index a5a1901..bbc89a1 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -177,13 +177,15 @@ def normalize_path(path: str, parent: str = os.curdir) -> str: str """ # NOTE(sigmavirus24): Using os.path.sep and os.path.altsep allow for - # Windows compatibility with both Windows-style paths (c:\\foo\bar) and + # Windows compatibility with both Windows-style paths (c:\foo\bar) and # Unix style paths (/foo/bar). separator = os.path.sep # NOTE(sigmavirus24): os.path.altsep may be None alternate_separator = os.path.altsep or "" - if separator in path or ( - alternate_separator and alternate_separator in path + if ( + path == "." + or separator in path + or (alternate_separator and alternate_separator in path) ): path = os.path.abspath(os.path.join(parent, path)) return path.rstrip(separator + alternate_separator) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 167ba72..5aadf2f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -132,6 +132,7 @@ def test_invalid_file_list(value): "value,expected", [ ("flake8", "flake8"), + (".", os.path.abspath(".")), ("../flake8", os.path.abspath("../flake8")), ("flake8/", os.path.abspath("flake8")), ], From 3b9b3325601a031bb0a130e0a6b5b678041a0d76 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 12:03:58 -0500 Subject: [PATCH 054/257] type api.legacy.Report --- src/flake8/api/legacy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 0d848fc..b529553 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -6,6 +6,7 @@ In 3.0 we no longer have an "engine" module but we maintain the API from it. import argparse import logging import os.path +from typing import List import flake8 from flake8.formatting import base as formatter @@ -183,21 +184,22 @@ class Report: .. versionchanged:: 3.0.0 """ - def __init__(self, application): + def __init__(self, application: app.Application) -> None: """Initialize the Report for the user. .. warning:: This should not be instantiated by users. """ + assert application.guide is not None self._application = application self._style_guide = application.guide self._stats = self._style_guide.stats @property - def total_errors(self): + def total_errors(self) -> int: """Return the total number of errors.""" return self._application.result_count - def get_statistics(self, violation): + def get_statistics(self, violation: str) -> List[str]: """Get the list of occurrences of a violation. :returns: From 1c3fef6cdaa95bc014783a013ca884b54333d9e9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 12:08:16 -0500 Subject: [PATCH 055/257] invert order of legacy to make it easier to type --- src/flake8/api/legacy.py | 146 +++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index b529553..9209252 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -19,43 +19,50 @@ LOG = logging.getLogger(__name__) __all__ = ("get_style_guide",) -def get_style_guide(**kwargs): - r"""Provision a StyleGuide for use. +class Report: + """Public facing object that mimic's Flake8 2.0's API. - :param \*\*kwargs: - Keyword arguments that provide some options for the StyleGuide. - :returns: - An initialized StyleGuide - :rtype: - :class:`StyleGuide` + .. note:: + + There are important changes in how this object behaves compared to + the object provided in Flake8 2.x. + + .. warning:: + + This should not be instantiated by users. + + .. versionchanged:: 3.0.0 """ - application = app.Application() - prelim_opts, remaining_args = application.parse_preliminary_options([]) - flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) - cfg, cfg_dir = config.load_config( - config=prelim_opts.config, - extra=prelim_opts.append_config, - isolated=prelim_opts.isolated, - ) + def __init__(self, application: app.Application) -> None: + """Initialize the Report for the user. - application.find_plugins(cfg, cfg_dir, prelim_opts.enable_extensions) - application.register_plugin_options() - application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) - # We basically want application.initialize to be called but with these - # options set instead before we make our formatter, notifier, internal - # style guide and file checker manager. - options = application.options - for key, value in kwargs.items(): - try: - getattr(options, key) - setattr(options, key, value) - except AttributeError: - LOG.error('Could not update option "%s"', key) - application.make_formatter() - application.make_guide() - application.make_file_checker_manager() - return StyleGuide(application) + .. warning:: This should not be instantiated by users. + """ + assert application.guide is not None + self._application = application + self._style_guide = application.guide + self._stats = self._style_guide.stats + + @property + def total_errors(self) -> int: + """Return the total number of errors.""" + return self._application.result_count + + def get_statistics(self, violation: str) -> List[str]: + """Get the list of occurrences of a violation. + + :returns: + List of occurrences of a violation formatted as: + {Count} {Error Code} {Message}, e.g., + ``8 E531 Some error message about the error`` + :rtype: + list + """ + return [ + f"{s.count} {s.error_code} {s.message}" + for s in self._stats.statistics_for(violation) + ] class StyleGuide: @@ -169,47 +176,40 @@ class StyleGuide: return self.check_files([filename]) -class Report: - """Public facing object that mimic's Flake8 2.0's API. +def get_style_guide(**kwargs): + r"""Provision a StyleGuide for use. - .. note:: - - There are important changes in how this object behaves compared to - the object provided in Flake8 2.x. - - .. warning:: - - This should not be instantiated by users. - - .. versionchanged:: 3.0.0 + :param \*\*kwargs: + Keyword arguments that provide some options for the StyleGuide. + :returns: + An initialized StyleGuide + :rtype: + :class:`StyleGuide` """ + application = app.Application() + prelim_opts, remaining_args = application.parse_preliminary_options([]) + flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) - def __init__(self, application: app.Application) -> None: - """Initialize the Report for the user. + cfg, cfg_dir = config.load_config( + config=prelim_opts.config, + extra=prelim_opts.append_config, + isolated=prelim_opts.isolated, + ) - .. warning:: This should not be instantiated by users. - """ - assert application.guide is not None - self._application = application - self._style_guide = application.guide - self._stats = self._style_guide.stats - - @property - def total_errors(self) -> int: - """Return the total number of errors.""" - return self._application.result_count - - def get_statistics(self, violation: str) -> List[str]: - """Get the list of occurrences of a violation. - - :returns: - List of occurrences of a violation formatted as: - {Count} {Error Code} {Message}, e.g., - ``8 E531 Some error message about the error`` - :rtype: - list - """ - return [ - f"{s.count} {s.error_code} {s.message}" - for s in self._stats.statistics_for(violation) - ] + application.find_plugins(cfg, cfg_dir, prelim_opts.enable_extensions) + application.register_plugin_options() + application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) + # We basically want application.initialize to be called but with these + # options set instead before we make our formatter, notifier, internal + # style guide and file checker manager. + options = application.options + for key, value in kwargs.items(): + try: + getattr(options, key) + setattr(options, key, value) + except AttributeError: + LOG.error('Could not update option "%s"', key) + application.make_formatter() + application.make_guide() + application.make_file_checker_manager() + return StyleGuide(application) From 78b2db4072cc1a81bf21391fcdd30638e8514792 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 12:33:39 -0500 Subject: [PATCH 056/257] type the rest of the legacy api --- setup.cfg | 4 --- src/flake8/api/legacy.py | 50 ++++++++++++++++++++++++++--------- tests/unit/test_legacy_api.py | 34 +++++------------------- 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9924505..2e0987c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -106,9 +106,5 @@ disallow_untyped_defs = true no_implicit_optional = true warn_unused_ignores = true -# TODO: fix these -[mypy-flake8.api.legacy] -disallow_untyped_defs = false - [mypy-tests.*] disallow_untyped_defs = false diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 9209252..ab20514 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -6,9 +6,13 @@ In 3.0 we no longer have an "engine" module but we maintain the API from it. import argparse import logging import os.path +from typing import Any from typing import List +from typing import Optional +from typing import Type import flake8 +from flake8.discover_files import expand_paths from flake8.formatting import base as formatter from flake8.main import application as app from flake8.options import config @@ -80,7 +84,7 @@ class StyleGuide: .. versionchanged:: 3.0.0 """ - def __init__(self, application): + def __init__(self, application: app.Application) -> None: """Initialize our StyleGuide.""" self._application = application self._file_checker_manager = application.file_checker_manager @@ -91,14 +95,16 @@ class StyleGuide: An instance of :class:`argparse.Namespace` containing parsed options. """ + assert self._application.options is not None return self._application.options @property - def paths(self): + def paths(self) -> List[str]: """Return the extra arguments passed as paths.""" - return self._application.paths + assert self._application.options is not None + return self._application.options.filenames - def check_files(self, paths=None): + def check_files(self, paths: Optional[List[str]] = None) -> Report: """Run collected checks on the files provided. This will check the files passed in and return a :class:`Report` @@ -111,12 +117,13 @@ class StyleGuide: :rtype: flake8.api.legacy.Report """ + assert self._application.options is not None self._application.options.filenames = paths self._application.run_checks() self._application.report_errors() return Report(self._application) - def excluded(self, filename, parent=None): + def excluded(self, filename: str, parent: Optional[str] = None) -> bool: """Determine if a file is excluded. :param str filename: @@ -128,14 +135,27 @@ class StyleGuide: :rtype: bool """ - return self._file_checker_manager.is_path_excluded(filename) or ( - parent - and self._file_checker_manager.is_path_excluded( - os.path.join(parent, filename) + + def excluded(path: str) -> bool: + paths = tuple( + expand_paths( + paths=[path], + stdin_display_name=self.options.stdin_display_name, + filename_patterns=self.options.filename, + exclude=self.options.exclude, + is_running_from_diff=self.options.diff, + ) ) + return not paths + + return excluded(filename) or ( + parent is not None and excluded(os.path.join(parent, filename)) ) - def init_report(self, reporter=None): + def init_report( + self, + reporter: Optional[Type[formatter.BaseFormatter]] = None, + ) -> None: """Set up a formatter for this run of Flake8.""" if reporter is None: return @@ -154,7 +174,13 @@ class StyleGuide: self._application.file_checker_manager = None self._application.make_file_checker_manager() - def input_file(self, filename, lines=None, expected=None, line_offset=0): + def input_file( + self, + filename: str, + lines: Optional[Any] = None, + expected: Optional[Any] = None, + line_offset: Optional[Any] = 0, + ) -> Report: """Run collected checks on a single file. This will check the file passed in and return a :class:`Report` @@ -176,7 +202,7 @@ class StyleGuide: return self.check_files([filename]) -def get_style_guide(**kwargs): +def get_style_guide(**kwargs: Any) -> StyleGuide: r"""Provision a StyleGuide for use. :param \*\*kwargs: diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 6424f60..4b44fb8 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -57,9 +57,9 @@ def test_styleguide_options(): def test_styleguide_paths(): """Show that we proxy the StyleGuide.paths attribute.""" app = mock.Mock() - app.paths = "paths" + app.options.filenames = ["paths"] style_guide = api.StyleGuide(app) - assert style_guide.paths == "paths" + assert style_guide.paths == ["paths"] def test_styleguide_check_files(): @@ -78,33 +78,13 @@ def test_styleguide_check_files(): def test_styleguide_excluded(): """Verify we delegate to our file checker manager. - We also want to ensure that if we don't specify a parent, is_path_excluded - is called exactly once. - """ - app = mock.Mock() - file_checker_manager = app.file_checker_manager = mock.Mock() - style_guide = api.StyleGuide(app) - - style_guide.excluded("file.py") - file_checker_manager.is_path_excluded.assert_called_once_with("file.py") - - -def test_styleguide_excluded_with_parent(): - """Verify we delegate to our file checker manager. - When we add the parent argument, we don't check that is_path_excluded was called only once. """ - app = mock.Mock() - file_checker_manager = app.file_checker_manager = mock.Mock() - file_checker_manager.is_path_excluded.return_value = False - style_guide = api.StyleGuide(app) - - style_guide.excluded("file.py", "parent") - assert file_checker_manager.is_path_excluded.call_args_list == [ - mock.call("file.py"), - mock.call(os.path.join("parent", "file.py")), - ] + style_guide = api.get_style_guide(exclude=["file*", "*/parent/*"]) + assert not style_guide.excluded("unrelated.py") + assert style_guide.excluded("file.py") + assert style_guide.excluded("test.py", "parent") def test_styleguide_init_report_does_nothing(): @@ -121,7 +101,7 @@ def test_styleguide_init_report_with_non_subclass(): app = mock.Mock() style_guide = api.StyleGuide(app) with pytest.raises(ValueError): - style_guide.init_report(object) + style_guide.init_report(object) # type: ignore assert app.make_formatter.called is False assert app.make_guide.called is False From 3c885219b56ec5755c44d1649be973ae206806de Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 13:02:38 -0500 Subject: [PATCH 057/257] use typesafe NamedTuple --- src/flake8/statistics.py | 9 +++++---- src/flake8/style_guide.py | 27 +++++++++++---------------- src/flake8/utils.py | 7 ++++++- tests/unit/test_style_guide.py | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index 073bfe4..ad93c8f 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -1,8 +1,8 @@ """Statistic collection logic for Flake8.""" -import collections from typing import Dict from typing import Generator from typing import List +from typing import NamedTuple from typing import Optional from typing import TYPE_CHECKING @@ -73,7 +73,7 @@ class Statistics: yield self._store[error_code] -class Key(collections.namedtuple("Key", ["filename", "code"])): +class Key(NamedTuple): """Simple key structure for the Statistics dictionary. To make things clearer, easier to read, and more understandable, we use a @@ -81,7 +81,8 @@ class Key(collections.namedtuple("Key", ["filename", "code"])): Statistics object. """ - __slots__ = () + filename: str + code: str @classmethod def create_from(cls, error: "Violation") -> "Key": @@ -111,7 +112,7 @@ class Statistic: """Simple wrapper around the logic of each statistic. Instead of maintaining a simple but potentially hard to reason about - tuple, we create a namedtuple which has attributes and a couple + tuple, we create a class which has attributes and a couple convenience methods on it. """ diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index c471fa6..9d45c79 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -1,6 +1,5 @@ """Implementation of the StyleGuide used by Flake8.""" import argparse -import collections import contextlib import copy import enum @@ -12,6 +11,7 @@ from typing import Dict from typing import Generator from typing import List from typing import Match +from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Set @@ -54,21 +54,16 @@ def find_noqa(physical_line: str) -> Optional[Match[str]]: return defaults.NOQA_INLINE_REGEXP.search(physical_line) -class Violation( - collections.namedtuple( - "Violation", - [ - "code", - "filename", - "line_number", - "column_number", - "text", - "physical_line", - ], - ) -): +class Violation(NamedTuple): """Class representing a violation reported by Flake8.""" + code: str + filename: str + line_number: int + column_number: int + text: str + physical_line: Optional[str] + def is_inline_ignored(self, disable_noqa: bool) -> bool: """Determine if a comment has been added to ignore this line. @@ -394,7 +389,7 @@ class StyleGuideManager: code: str, filename: str, line_number: int, - column_number: Optional[int], + column_number: int, text: str, physical_line: Optional[str] = None, ) -> int: @@ -527,7 +522,7 @@ class StyleGuide: code: str, filename: str, line_number: int, - column_number: Optional[int], + column_number: int, text: str, physical_line: Optional[str] = None, ) -> int: diff --git a/src/flake8/utils.py b/src/flake8/utils.py index bbc89a1..2506266 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -12,6 +12,7 @@ import textwrap import tokenize from typing import Dict from typing import List +from typing import NamedTuple from typing import Optional from typing import Pattern from typing import Sequence @@ -50,7 +51,11 @@ def parse_comma_separated_list( return [item for item in item_gen if item] -_Token = collections.namedtuple("_Token", ("tp", "src")) +class _Token(NamedTuple): + tp: str + src: str + + _CODE, _FILE, _COLON, _COMMA, _WS = "code", "file", "colon", "comma", "ws" _EOF = "eof" _FILE_LIST_TOKEN_TYPES = [ diff --git a/tests/unit/test_style_guide.py b/tests/unit/test_style_guide.py index da28355..e4aaff2 100644 --- a/tests/unit/test_style_guide.py +++ b/tests/unit/test_style_guide.py @@ -34,7 +34,7 @@ def test_handle_error_does_not_raise_type_errors(): ) assert 1 == guide.handle_error( - "T111", "file.py", 1, None, "error found", "a = 1" + "T111", "file.py", 1, 1, "error found", "a = 1" ) From fa4c31fb97b06f60075afacb82d59c24025dc794 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 13:36:22 -0500 Subject: [PATCH 058/257] break type checking cycles --- src/flake8/formatting/base.py | 19 ++-- src/flake8/formatting/default.py | 5 +- src/flake8/statistics.py | 10 +- src/flake8/style_guide.py | 96 +------------------ src/flake8/violation.py | 107 ++++++++++++++++++++++ tests/unit/test_base_formatter.py | 14 ++- tests/unit/test_filenameonly_formatter.py | 10 +- tests/unit/test_nothing_formatter.py | 6 +- tests/unit/test_statistics.py | 4 +- tests/unit/test_violation.py | 21 ++--- 10 files changed, 142 insertions(+), 150 deletions(-) create mode 100644 src/flake8/violation.py diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index a17cb44..f12018a 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -6,13 +6,10 @@ from typing import IO from typing import List from typing import Optional from typing import Tuple -from typing import TYPE_CHECKING from flake8.formatting import _windows_color - -if TYPE_CHECKING: - from flake8.statistics import Statistics - from flake8.style_guide import Violation +from flake8.statistics import Statistics +from flake8.violation import Violation class BaseFormatter: @@ -98,9 +95,9 @@ class BaseFormatter: :param error: This will be an instance of - :class:`~flake8.style_guide.Violation`. + :class:`~flake8.violation.Violation`. :type error: - flake8.style_guide.Violation + flake8.violation.Violation """ line = self.format(error) source = self.show_source(error) @@ -113,9 +110,9 @@ class BaseFormatter: :param error: This will be an instance of - :class:`~flake8.style_guide.Violation`. + :class:`~flake8.violation.Violation`. :type error: - flake8.style_guide.Violation + flake8.violation.Violation :returns: The formatted error string. :rtype: @@ -163,9 +160,9 @@ class BaseFormatter: :param error: This will be an instance of - :class:`~flake8.style_guide.Violation`. + :class:`~flake8.violation.Violation`. :type error: - flake8.style_guide.Violation + flake8.violation.Violation :returns: The formatted error string if the user wants to show the source. If the user does not want to show the source, this will return diff --git a/src/flake8/formatting/default.py b/src/flake8/formatting/default.py index 7c8073e..f43dc42 100644 --- a/src/flake8/formatting/default.py +++ b/src/flake8/formatting/default.py @@ -1,12 +1,9 @@ """Default formatting class for Flake8.""" from typing import Optional from typing import Set -from typing import TYPE_CHECKING from flake8.formatting import base - -if TYPE_CHECKING: - from flake8.style_guide import Violation +from flake8.violation import Violation COLORS = { "bold": "\033[1m", diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index ad93c8f..571d50b 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -4,10 +4,8 @@ from typing import Generator from typing import List from typing import NamedTuple from typing import Optional -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from flake8.style_guide import Violation +from flake8.violation import Violation class Statistics: @@ -34,7 +32,7 @@ class Statistics: The Violation instance containing the information about the violation. :type error: - flake8.style_guide.Violation + flake8.violation.Violation """ key = Key.create_from(error) if key not in self._store: @@ -86,7 +84,7 @@ class Key(NamedTuple): @classmethod def create_from(cls, error: "Violation") -> "Key": - """Create a Key from :class:`flake8.style_guide.Violation`.""" + """Create a Key from :class:`flake8.violation.Violation`.""" return cls(filename=error.filename, code=error.code) def matches(self, prefix: str, filename: Optional[str]) -> bool: @@ -127,7 +125,7 @@ class Statistic: @classmethod def create_from(cls, error: "Violation") -> "Statistic": - """Create a Statistic from a :class:`flake8.style_guide.Violation`.""" + """Create a Statistic from a :class:`flake8.violation.Violation`.""" return cls( error_code=error.code, filename=error.filename, diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 9d45c79..2f521aa 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -5,13 +5,10 @@ import copy import enum import functools import itertools -import linecache import logging from typing import Dict from typing import Generator from typing import List -from typing import Match -from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Set @@ -22,6 +19,7 @@ from flake8 import defaults from flake8 import statistics from flake8 import utils from flake8.formatting import base as base_formatter +from flake8.violation import Violation __all__ = ("StyleGuide",) @@ -49,98 +47,6 @@ class Decision(enum.Enum): Selected = "selected error" -@functools.lru_cache(maxsize=512) -def find_noqa(physical_line: str) -> Optional[Match[str]]: - return defaults.NOQA_INLINE_REGEXP.search(physical_line) - - -class Violation(NamedTuple): - """Class representing a violation reported by Flake8.""" - - code: str - filename: str - line_number: int - column_number: int - text: str - physical_line: Optional[str] - - def is_inline_ignored(self, disable_noqa: bool) -> bool: - """Determine if a comment has been added to ignore this line. - - :param bool disable_noqa: - Whether or not users have provided ``--disable-noqa``. - :returns: - True if error is ignored in-line, False otherwise. - :rtype: - bool - """ - physical_line = self.physical_line - # TODO(sigmavirus24): Determine how to handle stdin with linecache - if disable_noqa: - return False - - if physical_line is None: - physical_line = linecache.getline(self.filename, self.line_number) - noqa_match = find_noqa(physical_line) - if noqa_match is None: - LOG.debug("%r is not inline ignored", self) - return False - - codes_str = noqa_match.groupdict()["codes"] - if codes_str is None: - LOG.debug("%r is ignored by a blanket ``# noqa``", self) - return True - - codes = set(utils.parse_comma_separated_list(codes_str)) - if self.code in codes or self.code.startswith(tuple(codes)): - LOG.debug( - "%r is ignored specifically inline with ``# noqa: %s``", - self, - codes_str, - ) - return True - - LOG.debug( - "%r is not ignored inline with ``# noqa: %s``", self, codes_str - ) - return False - - def is_in(self, diff: Dict[str, Set[int]]) -> bool: - """Determine if the violation is included in a diff's line ranges. - - This function relies on the parsed data added via - :meth:`~StyleGuide.add_diff_ranges`. If that has not been called and - we are not evaluating files in a diff, then this will always return - True. If there are diff ranges, then this will return True if the - line number in the error falls inside one of the ranges for the file - (and assuming the file is part of the diff data). If there are diff - ranges, this will return False if the file is not part of the diff - data or the line number of the error is not in any of the ranges of - the diff. - - :returns: - True if there is no diff or if the error is in the diff's line - number ranges. False if the error's line number falls outside - the diff's line number ranges. - :rtype: - bool - """ - if not diff: - return True - - # NOTE(sigmavirus24): The parsed diff will be a defaultdict with - # a set as the default value (if we have received it from - # flake8.utils.parse_unified_diff). In that case ranges below - # could be an empty set (which is False-y) or if someone else - # is using this API, it could be None. If we could guarantee one - # or the other, we would check for it more explicitly. - line_numbers = diff.get(self.filename) - if not line_numbers: - return False - - return self.line_number in line_numbers - - class DecisionEngine: """A class for managing the decision process around violations. diff --git a/src/flake8/violation.py b/src/flake8/violation.py new file mode 100644 index 0000000..06983b1 --- /dev/null +++ b/src/flake8/violation.py @@ -0,0 +1,107 @@ +"""Contains the Violation error class used internally.""" +import functools +import linecache +import logging +from typing import Dict +from typing import Match +from typing import NamedTuple +from typing import Optional +from typing import Set + +from flake8 import defaults +from flake8 import utils + + +LOG = logging.getLogger(__name__) + + +@functools.lru_cache(maxsize=512) +def _find_noqa(physical_line: str) -> Optional[Match[str]]: + return defaults.NOQA_INLINE_REGEXP.search(physical_line) + + +class Violation(NamedTuple): + """Class representing a violation reported by Flake8.""" + + code: str + filename: str + line_number: int + column_number: int + text: str + physical_line: Optional[str] + + def is_inline_ignored(self, disable_noqa: bool) -> bool: + """Determine if a comment has been added to ignore this line. + + :param bool disable_noqa: + Whether or not users have provided ``--disable-noqa``. + :returns: + True if error is ignored in-line, False otherwise. + :rtype: + bool + """ + physical_line = self.physical_line + # TODO(sigmavirus24): Determine how to handle stdin with linecache + if disable_noqa: + return False + + if physical_line is None: + physical_line = linecache.getline(self.filename, self.line_number) + noqa_match = _find_noqa(physical_line) + if noqa_match is None: + LOG.debug("%r is not inline ignored", self) + return False + + codes_str = noqa_match.groupdict()["codes"] + if codes_str is None: + LOG.debug("%r is ignored by a blanket ``# noqa``", self) + return True + + codes = set(utils.parse_comma_separated_list(codes_str)) + if self.code in codes or self.code.startswith(tuple(codes)): + LOG.debug( + "%r is ignored specifically inline with ``# noqa: %s``", + self, + codes_str, + ) + return True + + LOG.debug( + "%r is not ignored inline with ``# noqa: %s``", self, codes_str + ) + return False + + def is_in(self, diff: Dict[str, Set[int]]) -> bool: + """Determine if the violation is included in a diff's line ranges. + + This function relies on the parsed data added via + :meth:`~StyleGuide.add_diff_ranges`. If that has not been called and + we are not evaluating files in a diff, then this will always return + True. If there are diff ranges, then this will return True if the + line number in the error falls inside one of the ranges for the file + (and assuming the file is part of the diff data). If there are diff + ranges, this will return False if the file is not part of the diff + data or the line number of the error is not in any of the ranges of + the diff. + + :returns: + True if there is no diff or if the error is in the diff's line + number ranges. False if the error's line number falls outside + the diff's line number ranges. + :rtype: + bool + """ + if not diff: + return True + + # NOTE(sigmavirus24): The parsed diff will be a defaultdict with + # a set as the default value (if we have received it from + # flake8.utils.parse_unified_diff). In that case ranges below + # could be an empty set (which is False-y) or if someone else + # is using this API, it could be None. If we could guarantee one + # or the other, we would check for it more explicitly. + line_numbers = diff.get(self.filename) + if not line_numbers: + return False + + return self.line_number in line_numbers diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py index d096457..7830eb4 100644 --- a/tests/unit/test_base_formatter.py +++ b/tests/unit/test_base_formatter.py @@ -5,9 +5,9 @@ from unittest import mock import pytest -from flake8 import style_guide from flake8.formatting import _windows_color from flake8.formatting import base +from flake8.violation import Violation def options(**kwargs): @@ -48,7 +48,7 @@ def test_format_needs_to_be_implemented(): formatter = base.BaseFormatter(options()) with pytest.raises(NotImplementedError): formatter.format( - style_guide.Violation("A000", "file.py", 1, 1, "error text", None) + Violation("A000", "file.py", 1, 1, "error text", None) ) @@ -57,9 +57,7 @@ def test_show_source_returns_nothing_when_not_showing_source(): formatter = base.BaseFormatter(options(show_source=False)) assert ( formatter.show_source( - style_guide.Violation( - "A000", "file.py", 1, 1, "error text", "line" - ) + Violation("A000", "file.py", 1, 1, "error text", "line") ) == "" ) @@ -70,7 +68,7 @@ def test_show_source_returns_nothing_when_there_is_source(): formatter = base.BaseFormatter(options(show_source=True)) assert ( formatter.show_source( - style_guide.Violation("A000", "file.py", 1, 1, "error text", None) + Violation("A000", "file.py", 1, 1, "error text", None) ) == "" ) @@ -99,7 +97,7 @@ def test_show_source_returns_nothing_when_there_is_source(): def test_show_source_updates_physical_line_appropriately(line1, line2, column): """Ensure the error column is appropriately indicated.""" formatter = base.BaseFormatter(options(show_source=True)) - error = style_guide.Violation("A000", "file.py", 1, column, "error", line1) + error = Violation("A000", "file.py", 1, column, "error", line1) output = formatter.show_source(error) assert output == line1 + line2 @@ -208,7 +206,7 @@ def test_handle_formats_the_error(): """Verify that a formatter will call format from handle.""" formatter = FormatFormatter(options(show_source=False)) filemock = formatter.output_fd = mock.Mock() - error = style_guide.Violation( + error = Violation( code="A001", filename="example.py", line_number=1, diff --git a/tests/unit/test_filenameonly_formatter.py b/tests/unit/test_filenameonly_formatter.py index 165ef69..e92d4bb 100644 --- a/tests/unit/test_filenameonly_formatter.py +++ b/tests/unit/test_filenameonly_formatter.py @@ -1,8 +1,8 @@ """Tests for the FilenameOnly formatter object.""" import argparse -from flake8 import style_guide from flake8.formatting import default +from flake8.violation import Violation def options(**kwargs): @@ -18,16 +18,14 @@ def test_caches_filenames_already_printed(): formatter = default.FilenameOnly(options()) assert formatter.filenames_already_printed == set() - formatter.format( - style_guide.Violation("code", "file.py", 1, 1, "text", "l") - ) + formatter.format(Violation("code", "file.py", 1, 1, "text", "l")) assert formatter.filenames_already_printed == {"file.py"} def test_only_returns_a_string_once_from_format(): """Verify format ignores the second error with the same filename.""" formatter = default.FilenameOnly(options()) - error = style_guide.Violation("code", "file.py", 1, 1, "text", "1") + error = Violation("code", "file.py", 1, 1, "text", "1") assert formatter.format(error) == "file.py" assert formatter.format(error) is None @@ -36,6 +34,6 @@ def test_only_returns_a_string_once_from_format(): def test_show_source_returns_nothing(): """Verify show_source returns nothing.""" formatter = default.FilenameOnly(options()) - error = style_guide.Violation("code", "file.py", 1, 1, "text", "1") + error = Violation("code", "file.py", 1, 1, "text", "1") assert formatter.show_source(error) is None diff --git a/tests/unit/test_nothing_formatter.py b/tests/unit/test_nothing_formatter.py index c019bdf..eb4b862 100644 --- a/tests/unit/test_nothing_formatter.py +++ b/tests/unit/test_nothing_formatter.py @@ -1,8 +1,8 @@ """Tests for the Nothing formatter obbject.""" import argparse -from flake8 import style_guide from flake8.formatting import default +from flake8.violation import Violation def options(**kwargs): @@ -16,7 +16,7 @@ def options(**kwargs): def test_format_returns_nothing(): """Verify Nothing.format returns None.""" formatter = default.Nothing(options()) - error = style_guide.Violation("code", "file.py", 1, 1, "text", "1") + error = Violation("code", "file.py", 1, 1, "text", "1") assert formatter.format(error) is None @@ -24,6 +24,6 @@ def test_format_returns_nothing(): def test_show_source_returns_nothing(): """Verify Nothing.show_source returns None.""" formatter = default.Nothing(options()) - error = style_guide.Violation("code", "file.py", 1, 1, "text", "1") + error = Violation("code", "file.py", 1, 1, "text", "1") assert formatter.show_source(error) is None diff --git a/tests/unit/test_statistics.py b/tests/unit/test_statistics.py index 9937916..03f3189 100644 --- a/tests/unit/test_statistics.py +++ b/tests/unit/test_statistics.py @@ -2,7 +2,7 @@ import pytest from flake8 import statistics as stats -from flake8 import style_guide +from flake8.violation import Violation DEFAULT_ERROR_CODE = "E100" DEFAULT_FILENAME = "file.py" @@ -16,7 +16,7 @@ def make_error(**kwargs): kwargs.setdefault("line_number", 1) kwargs.setdefault("column_number", 1) kwargs.setdefault("text", DEFAULT_TEXT) - return style_guide.Violation(**kwargs, physical_line=None) + return Violation(**kwargs, physical_line=None) def test_key_creation(): diff --git a/tests/unit/test_violation.py b/tests/unit/test_violation.py index b9cf1a3..6b47691 100644 --- a/tests/unit/test_violation.py +++ b/tests/unit/test_violation.py @@ -1,9 +1,9 @@ -"""Tests for the flake8.style_guide.Violation class.""" +"""Tests for the flake8.violation.Violation class.""" from unittest import mock import pytest -from flake8 import style_guide +from flake8.violation import Violation @pytest.mark.parametrize( @@ -33,9 +33,7 @@ from flake8 import style_guide ) def test_is_inline_ignored(error_code, physical_line, expected_result): """Verify that we detect inline usage of ``# noqa``.""" - error = style_guide.Violation( - error_code, "filename.py", 1, 1, "error text", None - ) + error = Violation(error_code, "filename.py", 1, 1, "error text", None) # We want `None` to be passed as the physical line so we actually use our # monkey-patched linecache.getline value. @@ -45,9 +43,7 @@ def test_is_inline_ignored(error_code, physical_line, expected_result): def test_disable_is_inline_ignored(): """Verify that is_inline_ignored exits immediately if disabling NoQA.""" - error = style_guide.Violation( - "E121", "filename.py", 1, 1, "error text", "line" - ) + error = Violation("E121", "filename.py", 1, 1, "error text", "line") with mock.patch("linecache.getline") as getline: assert error.is_inline_ignored(True) is False @@ -67,13 +63,8 @@ def test_disable_is_inline_ignored(): ) def test_violation_is_in_diff(violation_file, violation_line, diff, expected): """Verify that we find violations within a diff.""" - violation = style_guide.Violation( - "E001", - violation_file, - violation_line, - 1, - "warning", - "line", + violation = Violation( + "E001", violation_file, violation_line, 1, "warning", "line" ) assert violation.is_in(diff) is expected From 741ff11bfbf287b702bccac36b8c2eda642855d8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 14:07:13 -0500 Subject: [PATCH 059/257] use type hints instead of :type and :rtype --- docs/source/conf.py | 4 ++- docs/source/internal/writing-code.rst | 40 +++------------------ src/flake8/__init__.py | 4 +-- src/flake8/api/legacy.py | 22 ++++-------- src/flake8/checker.py | 4 +-- src/flake8/discover_files.py | 4 +-- src/flake8/formatting/base.py | 20 +++-------- src/flake8/main/application.py | 12 ++----- src/flake8/main/cli.py | 2 +- src/flake8/main/options.py | 3 +- src/flake8/options/manager.py | 40 ++++++++++----------- src/flake8/processor.py | 5 +-- src/flake8/statistics.py | 14 +++----- src/flake8/style_guide.py | 50 ++++++++++----------------- src/flake8/utils.py | 31 ++++------------- src/flake8/violation.py | 6 +--- 16 files changed, 77 insertions(+), 184 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9f0795b..2c9ea0b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ import flake8 # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "1.3" +needs_sphinx = "2.1" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -306,3 +306,5 @@ extlinks = { "issue": ("https://github.com/pycqa/flake8/issues/%s", "#"), "pull": ("https://github.com/pycqa/flake8/pull/%s", "#"), } + +autodoc_typehints = "description" diff --git a/docs/source/internal/writing-code.rst b/docs/source/internal/writing-code.rst index daf1d57..323e107 100644 --- a/docs/source/internal/writing-code.rst +++ b/docs/source/internal/writing-code.rst @@ -34,23 +34,21 @@ accepts as well as what it returns. .. code-block:: python # src/flake8/main/git.py - def hook(lazy=False, strict=False): + def hook(lazy: bool = False, strict: bool = False) -> int: """Execute Flake8 on the files in git's index. Determine which files are about to be committed and run Flake8 over them to check for violations. - :param bool lazy: + :param lazy: Find files not added to the index prior to committing. This is useful if you frequently use ``git commit -a`` for example. This defaults to False since it will otherwise include files not in the index. - :param bool strict: + :param strict: If True, return the total number of errors/violations found by Flake8. This will cause the hook to fail. :returns: Total number of errors found during the run. - :rtype: - int """ # NOTE(sigmavirus24): Delay import of application until we need it. from flake8.main import application @@ -66,39 +64,9 @@ accepts as well as what it returns. return app.result_count return 0 -Note that because the parameters ``hook`` and ``strict`` are simply boolean -parameters, we inline the type declaration for those parameters, e.g., - -.. code-block:: restructuredtext - - :param bool lazy: - -Also note that we begin the description of the parameter on a new-line and +Note that we begin the description of the parameter on a new-line and indented 4 spaces. -On the other hand, we also separate the parameter type declaration in some -places where the name is a little longer, e.g., - -.. code-block:: python - - # src/flake8/formatting/base.py - def format(self, error): - """Format an error reported by Flake8. - - This method **must** be implemented by subclasses. - - :param error: - This will be an instance of :class:`~flake8.style_guide.Error`. - :type error: - flake8.style_guide.Error - :returns: - The formatted error string. - :rtype: - str - """ - -Here we've separated ``:param error:`` and ``:type error:``. - Following the above examples and guidelines should help you write doc-strings that are stylistically correct for |Flake8|. diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 53bc407..47523d0 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -48,9 +48,9 @@ def configure_logging( ) -> None: """Configure logging for flake8. - :param int verbosity: + :param verbosity: How verbose to be in logging information. - :param str filename: + :param filename: Name of the file to append log information to. If ``None`` this will log to ``sys.stderr``. If the name is "stdout" or "stderr" this will log to the appropriate diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index ab20514..089543d 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -60,8 +60,6 @@ class Report: List of occurrences of a violation formatted as: {Count} {Error Code} {Message}, e.g., ``8 E531 Some error message about the error`` - :rtype: - list """ return [ f"{s.count} {s.error_code} {s.message}" @@ -110,12 +108,10 @@ class StyleGuide: This will check the files passed in and return a :class:`Report` instance. - :param list paths: + :param paths: List of filenames (or paths) to check. :returns: Object that mimic's Flake8 2.0's Reporter class. - :rtype: - flake8.api.legacy.Report """ assert self._application.options is not None self._application.options.filenames = paths @@ -126,14 +122,12 @@ class StyleGuide: def excluded(self, filename: str, parent: Optional[str] = None) -> bool: """Determine if a file is excluded. - :param str filename: + :param filename: Path to the file to check if it is excluded. - :param str parent: + :param parent: Name of the parent directory containing the file. :returns: True if the filename is excluded, False otherwise. - :rtype: - bool """ def excluded(path: str) -> bool: @@ -186,18 +180,16 @@ class StyleGuide: This will check the file passed in and return a :class:`Report` instance. - :param str filename: + :param filename: The path to the file to check. - :param list lines: + :param lines: Ignored since Flake8 3.0. :param expected: Ignored since Flake8 3.0. - :param int line_offset: + :param line_offset: Ignored since Flake8 3.0. :returns: Object that mimic's Flake8 2.0's Reporter class. - :rtype: - flake8.api.legacy.Report """ return self.check_files([filename]) @@ -209,8 +201,6 @@ def get_style_guide(**kwargs: Any) -> StyleGuide: Keyword arguments that provide some options for the StyleGuide. :returns: An initialized StyleGuide - :rtype: - :class:`StyleGuide` """ application = app.Application() prelim_opts, remaining_args = application.parse_preliminary_options([]) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index a42523d..e9a8dd9 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -182,8 +182,6 @@ class Manager: :returns: A tuple of the total results found and the results reported. - :rtype: - tuple(int, int) """ results_reported = results_found = 0 for checker in self._all_checkers: @@ -260,7 +258,7 @@ class Manager: def start(self, paths: Optional[List[str]] = None) -> None: """Start checking files. - :param list paths: + :param paths: Path names to check. This is passed directly to :meth:`~Manager.make_checkers`. """ diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py index f51b9d0..8c21064 100644 --- a/src/flake8/discover_files.py +++ b/src/flake8/discover_files.py @@ -17,9 +17,9 @@ def _filenames_from( ) -> Generator[str, None, None]: """Generate filenames from an argument. - :param str arg: + :param arg: Parameter from the command-line. - :param callable predicate: + :param predicate: Predicate to use to filter out filenames. If the predicate returns ``True`` we will exclude the filename, otherwise we will yield it. By default, we include every filename diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index f12018a..78d10e9 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -43,8 +43,6 @@ class BaseFormatter: :param options: User specified configuration parsed from both configuration files and the command-line interface. - :type options: - :class:`argparse.Namespace` """ self.options = options self.filename = options.output_file @@ -63,7 +61,7 @@ class BaseFormatter: def beginning(self, filename: str) -> None: """Notify the formatter that we're starting to process a file. - :param str filename: + :param filename: The name of the file that Flake8 is beginning to report results from. """ @@ -71,7 +69,7 @@ class BaseFormatter: def finished(self, filename: str) -> None: """Notify the formatter that we've finished processing a file. - :param str filename: + :param filename: The name of the file that Flake8 has finished reporting results from. """ @@ -96,8 +94,6 @@ class BaseFormatter: :param error: This will be an instance of :class:`~flake8.violation.Violation`. - :type error: - flake8.violation.Violation """ line = self.format(error) source = self.show_source(error) @@ -111,12 +107,8 @@ class BaseFormatter: :param error: This will be an instance of :class:`~flake8.violation.Violation`. - :type error: - flake8.violation.Violation :returns: The formatted error string. - :rtype: - str """ raise NotImplementedError( "Subclass of BaseFormatter did not implement" " format." @@ -161,14 +153,10 @@ class BaseFormatter: :param error: This will be an instance of :class:`~flake8.violation.Violation`. - :type error: - flake8.violation.Violation :returns: The formatted error string if the user wants to show the source. If the user does not want to show the source, this will return ``None``. - :rtype: - str """ if not self.options.show_source or error.physical_line is None: return "" @@ -197,9 +185,9 @@ class BaseFormatter: out for subclasses. Override this if you want behaviour that differs from the default. - :param str line: + :param line: The formatted string to print or write. - :param str source: + :param source: The source code that has been formatted and associated with the line of output. """ diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 4b91145..b9a0e66 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -35,13 +35,7 @@ class Application: """Abstract our application into a class.""" def __init__(self) -> None: - """Initialize our application. - - :param str program: - The name of the program/application that we're executing. - :param str version: - The version of the program/application we're executing. - """ + """Initialize our application.""" #: The timestamp when the Application instance was instantiated. self.start_time = time.time() #: The timestamp when the Application finished reported errors. @@ -91,12 +85,10 @@ class Application: options; we ignore those for now, they'll be parsed later when we do real option parsing. - :param list argv: + :param argv: Command-line arguments passed in directly. :returns: Populated namespace and list of remaining argument strings. - :rtype: - (argparse.Namespace, list) """ args, rest = self.prelim_arg_parser.parse_known_args(argv) # XXX (ericvw): Special case "forwarding" the output file option so diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py index 8d7ea9a..b4bb202 100644 --- a/src/flake8/main/cli.py +++ b/src/flake8/main/cli.py @@ -12,7 +12,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: This handles the creation of an instance of :class:`Application`, runs it, and then exits the application. - :param list argv: + :param argv: The arguments to be passed to the application for parsing. """ if argv is None: diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 93178ac..201483b 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -77,8 +77,7 @@ class JobsArgument: def __init__(self, arg: str) -> None: """Parse and validate the --jobs argument. - :param str arg: - The argument passed by argparse for validation + :param arg: The argument passed by argparse for validation """ self.is_auto = False self.n_jobs = -1 diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index ad5543a..896470d 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -125,10 +125,10 @@ class Option: The following are all passed directly through to argparse. - :param str short_option_name: + :param short_option_name: The short name of the option (e.g., ``-x``). This will be the first argument passed to ``ArgumentParser.add_argument`` - :param str long_option_name: + :param long_option_name: The long name of the option (e.g., ``--xtra-long-option``). This will be the second argument passed to ``ArgumentParser.add_argument`` @@ -141,13 +141,13 @@ class Option: :param const: Constant value to store on a common destination. Usually used in conjunction with ``action="store_const"``. - :param iterable choices: + :param choices: Possible values for the option. - :param str help: + :param help: Help text displayed in the usage information. - :param str metavar: + :param metavar: Name to use instead of the long option name for help text. - :param bool required: + :param required: Whether this option is required or not. The following options may be passed directly through to :mod:`argparse` @@ -157,28 +157,28 @@ class Option: A callable to normalize the type (as is the case in :mod:`argparse`). Deprecated: you can also pass through type strings such as ``'int'`` which are handled by :mod:`optparse`. - :param str action: + :param action: Any action allowed by :mod:`argparse`. Deprecated: this also understands the ``action='callback'`` action from :mod:`optparse`. - :param callable callback: + :param callback: Callback used if the action is ``"callback"``. Deprecated: please use ``action=`` instead. - :param iterable callback_args: + :param callback_args: Additional positional arguments to the callback callable. Deprecated: please use ``action=`` instead (probably with ``functools.partial``). - :param dictionary callback_kwargs: + :param callback_kwargs: Keyword arguments to the callback callable. Deprecated: please use ``action=`` instead (probably with ``functools.partial``). The following parameters are for Flake8's option handling alone. - :param bool parse_from_config: + :param parse_from_config: Whether or not this option should be parsed out of config files. - :param bool comma_separated_list: + :param comma_separated_list: Whether the option is a comma separated list when parsing from a config file. - :param bool normalize_paths: + :param normalize_paths: Whether the option is expecting a path or list of paths and should attempt to normalize the paths to absolute paths. """ @@ -325,13 +325,13 @@ class OptionManager: ) -> None: """Initialize an instance of an OptionManager. - :param str prog: + :param prog: Name of the actual program (e.g., flake8). - :param str version: + :param version: Version string for the program. - :param str usage: + :param usage: Basic usage string used by the OptionParser. - :param argparse.ArgumentParser parents: + :param parents: A list of ArgumentParser objects whose arguments should also be included. """ @@ -410,7 +410,7 @@ class OptionManager: def remove_from_default_ignore(self, error_codes: Sequence[str]) -> None: """Remove specified error codes from the default ignore list. - :param list error_codes: + :param error_codes: List of strings that are the error/warning codes to attempt to remove from the extended default ignore list. """ @@ -428,7 +428,7 @@ class OptionManager: def extend_default_ignore(self, error_codes: Sequence[str]) -> None: """Extend the default ignore list with the error codes provided. - :param list error_codes: + :param error_codes: List of strings that are the error/warning codes with which to extend the default ignore list. """ @@ -438,7 +438,7 @@ class OptionManager: def extend_default_select(self, error_codes: Sequence[str]) -> None: """Extend the default select list with the error codes provided. - :param list error_codes: + :param error_codes: List of strings that are the error/warning codes with which to extend the default select list. """ diff --git a/src/flake8/processor.py b/src/flake8/processor.py index d0652d8..fe0f53b 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -67,8 +67,7 @@ class FileProcessor: ) -> None: """Initialice our file processor. - :param str filename: - Name of the file to process + :param filename: Name of the file to process """ self.options = options self.filename = filename @@ -355,8 +354,6 @@ class FileProcessor: :returns: True if a line matches :attr:`defaults.NOQA_FILE`, otherwise False - :rtype: - bool """ if not self.options.disable_noqa and any( defaults.NOQA_FILE.match(line) for line in self.lines diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index 571d50b..ae89be1 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -20,8 +20,6 @@ class Statistics: :returns: Sorted list of error codes. - :rtype: - list(str) """ return sorted({key.code for key in self._store}) @@ -31,8 +29,6 @@ class Statistics: :param error: The Violation instance containing the information about the violation. - :type error: - flake8.violation.Violation """ key = Key.create_from(error) if key not in self._store: @@ -57,9 +53,9 @@ class Statistics: >>> stats.statistics_for('W') - :param str prefix: + :param prefix: The error class or specific error code to find statistics for. - :param str filename: + :param filename: (Optional) The filename to further filter results by. :returns: Generator of instances of :class:`Statistic` @@ -90,16 +86,14 @@ class Key(NamedTuple): def matches(self, prefix: str, filename: Optional[str]) -> bool: """Determine if this key matches some constraints. - :param str prefix: + :param prefix: The error code prefix that this key's error code should start with. - :param str filename: + :param filename: The filename that we potentially want to match on. This can be None to only match on error prefix. :returns: True if the Key's code starts with the prefix and either filename is None, or the Key's filename matches the value passed in. - :rtype: - bool """ return self.code.startswith(prefix) and ( filename is None or self.filename == filename diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 2f521aa..66906fd 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -89,8 +89,7 @@ class DecisionEngine: def was_selected(self, code: str) -> Union[Selected, Ignored]: """Determine if the code has been selected by the user. - :param str code: - The code for the check that has been run. + :param code: The code for the check that has been run. :returns: Selected.Implicitly if the selected list is empty, Selected.Explicitly if the selected list is not empty and a match @@ -112,7 +111,7 @@ class DecisionEngine: def was_ignored(self, code: str) -> Union[Selected, Ignored]: """Determine if the code has been ignored by the user. - :param str code: + :param code: The code for the check that has been run. :returns: Selected.Implicitly if the ignored list is empty, @@ -211,8 +210,7 @@ class DecisionEngine: This method does not look at whether the specific line is being ignored in the file itself. - :param str code: - The code for the check that has been run. + :param code: The code for the check that has been run. """ decision = self.cache.get(code) if decision is None: @@ -257,12 +255,8 @@ class StyleGuideManager: :param options: The original options parsed from the CLI and config file. - :type options: - :class:`~argparse.Namespace` :returns: A copy of the default style guide with overridden values. - :rtype: - :class:`~flake8.style_guide.StyleGuide` """ per_file = utils.parse_files_to_codes_mapping(options.per_file_ignores) for filename, violations in per_file: @@ -301,25 +295,23 @@ class StyleGuideManager: ) -> int: """Handle an error reported by a check. - :param str code: + :param code: The error code found, e.g., E123. - :param str filename: + :param filename: The file in which the error was found. - :param int line_number: + :param line_number: The line number (where counting starts at 1) at which the error occurs. - :param int column_number: + :param column_number: The column number (where counting starts at 1) at which the error occurs. - :param str text: + :param text: The text of the error message. - :param str physical_line: + :param physical_line: The actual physical line causing the error. :returns: 1 if the error was reported. 0 if it was ignored. This is to allow for counting of the number of errors found that were not ignored. - :rtype: - int """ guide = self.style_guide_for(filename) return guide.handle_error( @@ -332,7 +324,7 @@ class StyleGuideManager: This provides information to the underlying StyleGuides so that only the errors in the line number ranges are reported. - :param dict diffinfo: + :param diffinfo: Dictionary mapping filenames to sets of line number ranges. """ for guide in self.style_guides: @@ -392,13 +384,11 @@ class StyleGuide: def applies_to(self, filename: str) -> bool: """Check if this StyleGuide applies to the file. - :param str filename: + :param filename: The name of the file with violations that we're potentially applying this StyleGuide to. :returns: True if this applies, False otherwise - :rtype: - bool """ if self.filename is None: return True @@ -418,7 +408,7 @@ class StyleGuide: This method does not look at whether the specific line is being ignored in the file itself. - :param str code: + :param code: The code for the check that has been run. """ return self.decider.decision_for(code) @@ -434,25 +424,23 @@ class StyleGuide: ) -> int: """Handle an error reported by a check. - :param str code: + :param code: The error code found, e.g., E123. - :param str filename: + :param filename: The file in which the error was found. - :param int line_number: + :param line_number: The line number (where counting starts at 1) at which the error occurs. - :param int column_number: + :param column_number: The column number (where counting starts at 1) at which the error occurs. - :param str text: + :param text: The text of the error message. - :param str physical_line: + :param physical_line: The actual physical line causing the error. :returns: 1 if the error was reported. 0 if it was ignored. This is to allow for counting of the number of errors found that were not ignored. - :rtype: - int """ disable_noqa = self.options.disable_noqa # NOTE(sigmavirus24): Apparently we're provided with 0-indexed column @@ -485,7 +473,7 @@ class StyleGuide: This provides information to the StyleGuide so that only the errors in the line number ranges are reported. - :param dict diffinfo: + :param diffinfo: Dictionary mapping filenames to sets of line number ranges. """ self._parsed_diff = diffinfo diff --git a/src/flake8/utils.py b/src/flake8/utils.py index 2506266..71f9dd0 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -37,12 +37,8 @@ def parse_comma_separated_list( :param regexp: Compiled regular expression used to split the value when it is a string. - :type regexp: - _sre.SRE_Pattern :returns: List of values with whitespace stripped. - :rtype: - list """ assert isinstance(value, str), value @@ -94,7 +90,6 @@ def parse_files_to_codes_mapping( # noqa: C901 either comma or whitespace tokens. :param value: String to be parsed and normalized. - :type value: str """ if not isinstance(value_, str): value = "\n".join(value_) @@ -166,8 +161,6 @@ def normalize_paths( :returns: The normalized paths. - :rtype: - [str] """ assert isinstance(paths, list), paths return [normalize_path(p, parent) for p in paths] @@ -178,8 +171,6 @@ def normalize_path(path: str, parent: str = os.curdir) -> str: :returns: The normalized path. - :rtype: - str """ # NOTE(sigmavirus24): Using os.path.sep and os.path.altsep allow for # Windows compatibility with both Windows-style paths (c:\foo\bar) and @@ -219,8 +210,6 @@ def parse_unified_diff(diff: Optional[str] = None) -> Dict[str, Set[int]]: :returns: dictionary mapping file names to sets of line numbers - :rtype: - dict """ # Allow us to not have to patch out stdin_get_value if diff is None: @@ -284,12 +273,10 @@ def parse_unified_diff(diff: Optional[str] = None) -> Dict[str, Set[int]]: def is_using_stdin(paths: List[str]) -> bool: """Determine if we're going to read from stdin. - :param list paths: + :param paths: The paths that we're going to check. :returns: True if stdin (-) is in the path, otherwise False - :rtype: - bool """ return "-" in paths @@ -297,11 +284,11 @@ def is_using_stdin(paths: List[str]) -> bool: def fnmatch(filename: str, patterns: Sequence[str]) -> bool: """Wrap :func:`fnmatch.fnmatch` to add some functionality. - :param str filename: + :param filename: Name of the file we're trying to match. - :param list patterns: + :param patterns: Patterns we're using to try to match the filename. - :param bool default: + :param default: The default value if patterns is empty :returns: True if a pattern matches the filename, False if it doesn't. @@ -320,18 +307,14 @@ def matches_filename( ) -> bool: """Use fnmatch to discern if a path exists in patterns. - :param str path: + :param path: The path to the file under question :param patterns: The patterns to match the path against. - :type patterns: - list[str] - :param str log_message: + :param log_message: The message used for logging purposes. :returns: True if path matches patterns, False otherwise - :rtype: - bool """ if not patterns: return False @@ -354,8 +337,6 @@ def get_python_version() -> str: :returns: Implementation name, version, and platform as a string. - :rtype: - str """ return "{} {} on {}".format( platform.python_implementation(), diff --git a/src/flake8/violation.py b/src/flake8/violation.py index 06983b1..d2d2578 100644 --- a/src/flake8/violation.py +++ b/src/flake8/violation.py @@ -33,12 +33,10 @@ class Violation(NamedTuple): def is_inline_ignored(self, disable_noqa: bool) -> bool: """Determine if a comment has been added to ignore this line. - :param bool disable_noqa: + :param disable_noqa: Whether or not users have provided ``--disable-noqa``. :returns: True if error is ignored in-line, False otherwise. - :rtype: - bool """ physical_line = self.physical_line # TODO(sigmavirus24): Determine how to handle stdin with linecache @@ -88,8 +86,6 @@ class Violation(NamedTuple): True if there is no diff or if the error is in the diff's line number ranges. False if the error's line number falls outside the diff's line number ranges. - :rtype: - bool """ if not diff: return True From ec57d5e67ce7f8dfb83df7563dc777ee691b60b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 15:37:25 -0500 Subject: [PATCH 060/257] use tokenize.TokenInfo instead of _Token alias --- src/flake8/checker.py | 2 +- src/flake8/plugins/pyflakes.py | 7 +++++-- src/flake8/processor.py | 21 +++++++++++---------- tests/unit/test_file_processor.py | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index e9a8dd9..2d6421f 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -554,7 +554,7 @@ class FileChecker: self.run_logical_checks() def check_physical_eol( - self, token: processor._Token, prev_physical: str + self, token: tokenize.TokenInfo, prev_physical: str ) -> None: """Run physical checks if and only if it is at the end of the line.""" assert self.processor is not None diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index ba3493a..dc8a02e 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -2,6 +2,7 @@ import argparse import ast import os +import tokenize from typing import Any from typing import Generator from typing import List @@ -12,7 +13,6 @@ import pyflakes.checker from flake8 import utils from flake8.options.manager import OptionManager -from flake8.processor import _Token FLAKE8_PYFLAKES_CODES = { "UnusedImport": "F401", @@ -74,7 +74,10 @@ class FlakesChecker(pyflakes.checker.Checker): exclude_from_doctest: List[str] = [] def __init__( - self, tree: ast.AST, file_tokens: List[_Token], filename: str + self, + tree: ast.AST, + file_tokens: List[tokenize.TokenInfo], + filename: str, ) -> None: """Initialize the PyFlakes plugin with an AST tree and filename.""" filename = utils.normalize_path(filename) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index fe0f53b..40b3266 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -23,7 +23,6 @@ SKIP_TOKENS = frozenset( [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT] ) -_Token = Tuple[int, str, Tuple[int, int], Tuple[int, int], str] _LogicalMapping = List[Tuple[int, Tuple[int, int]]] _Logical = Tuple[List[str], List[str], _LogicalMapping] @@ -108,19 +107,19 @@ class FileProcessor: #: Previous unindented (i.e. top-level) logical line self.previous_unindented_logical_line = "" #: Current set of tokens - self.tokens: List[_Token] = [] + self.tokens: List[tokenize.TokenInfo] = [] #: Total number of lines in the file self.total_lines = len(self.lines) #: Verbosity level of Flake8 self.verbose = options.verbose #: Statistics dictionary self.statistics = {"logical lines": 0} - self._file_tokens: Optional[List[_Token]] = None + self._file_tokens: Optional[List[tokenize.TokenInfo]] = None # map from line number to the line we'll search for `noqa` in self._noqa_line_mapping: Optional[Dict[int, str]] = None @property - def file_tokens(self) -> List[_Token]: + def file_tokens(self) -> List[tokenize.TokenInfo]: """Return the complete set of tokens for a file.""" if self._file_tokens is None: line_iter = iter(self.lines) @@ -227,7 +226,9 @@ class FileProcessor: self.statistics["logical lines"] += 1 return joined_comments, self.logical_line, mapping_list - def split_line(self, token: _Token) -> Generator[str, None, None]: + def split_line( + self, token: tokenize.TokenInfo + ) -> Generator[str, None, None]: """Split a physical line's line based on new-lines. This also auto-increments the line number for the caller. @@ -261,7 +262,7 @@ class FileProcessor: ) return arguments - def generate_tokens(self) -> Generator[_Token, None, None]: + def generate_tokens(self) -> Generator[tokenize.TokenInfo, None, None]: """Tokenize the file and yield the tokens.""" for token in tokenize.generate_tokens(self.next_line): if token[2][0] > self.total_lines: @@ -385,17 +386,17 @@ class FileProcessor: self.lines[0] = self.lines[0][3:] -def is_eol_token(token: _Token) -> bool: +def is_eol_token(token: tokenize.TokenInfo) -> bool: """Check if the token is an end-of-line token.""" return token[0] in NEWLINE or token[4][token[3][1] :].lstrip() == "\\\n" -def is_multiline_string(token: _Token) -> bool: +def is_multiline_string(token: tokenize.TokenInfo) -> bool: """Check if this is a multiline string.""" return token[0] == tokenize.STRING and "\n" in token[1] -def token_is_newline(token: _Token) -> bool: +def token_is_newline(token: tokenize.TokenInfo) -> bool: """Check if the token type is a newline token type.""" return token[0] in NEWLINE @@ -409,7 +410,7 @@ def count_parentheses(current_parentheses_count: int, token_text: str) -> int: return current_parentheses_count -def log_token(log: logging.Logger, token: _Token) -> None: +def log_token(log: logging.Logger, token: tokenize.TokenInfo) -> None: """Log a token to a provided logging object.""" if token[2][0] == token[3][0]: pos = "[{}:{}]".format(token[2][1] or "", token[3][1]) diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index 789135a..78740fc 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -275,7 +275,7 @@ def test_split_line(unsplit_line, expected_lines, default_options): ], ) - token = (1, unsplit_line, (0, 0), (0, 0), "") + token = tokenize.TokenInfo(1, unsplit_line, (0, 0), (0, 0), "") actual_lines = list(file_processor.split_line(token)) assert expected_lines == actual_lines From af07ecbf96d35cc53a7f00b85d57c2b9f8a48407 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 16:00:01 -0500 Subject: [PATCH 061/257] fix docs build --- docs/source/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index cfcfbdf..da3b991 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1,4 +1,4 @@ -sphinx>=1.3.0,!=3.1.0 +sphinx>=2.1.0,!=3.1.0 sphinx_rtd_theme sphinx-prompt docutils!=0.18 From 5ca854a615a70715224797b5d9c47ab23275b18f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 18:56:53 -0800 Subject: [PATCH 062/257] simplify a bit of code in style_guide.py --- src/flake8/style_guide.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 66906fd..f26e738 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -78,14 +78,6 @@ class DecisionEngine: ).union(options.extended_default_ignore) self.using_default_select = set(self.selected) == set(defaults.SELECT) - def _in_all_selected(self, code: str) -> bool: - return bool(self.all_selected) and code.startswith(self.all_selected) - - def _in_extended_selected(self, code: str) -> bool: - return bool(self.extended_selected) and code.startswith( - self.extended_selected - ) - def was_selected(self, code: str) -> Union[Selected, Ignored]: """Determine if the code has been selected by the user. @@ -97,10 +89,10 @@ class DecisionEngine: Ignored.Implicitly if the selected list is not empty but no match was found. """ - if self._in_all_selected(code): + if code.startswith(self.all_selected): return Selected.Explicitly - if not self.all_selected and self._in_extended_selected(code): + if not self.all_selected and code.startswith(self.extended_selected): # If it was not explicitly selected, it may have been implicitly # selected because the check comes from a plugin that is enabled by # default @@ -120,7 +112,7 @@ class DecisionEngine: Selected.Implicitly if the ignored list is not empty but no match was found. """ - if self.ignored and code.startswith(self.ignored): + if code.startswith(self.ignored): return Ignored.Explicitly return Selected.Implicitly @@ -444,8 +436,7 @@ class StyleGuide: """ disable_noqa = self.options.disable_noqa # NOTE(sigmavirus24): Apparently we're provided with 0-indexed column - # numbers so we have to offset that here. Also, if a SyntaxError is - # caught, column_number may be None. + # numbers so we have to offset that here. if not column_number: column_number = 0 error = Violation( @@ -491,7 +482,6 @@ def find_first_match( startswith = error_code.startswith for code in code_list: if startswith(code): - break + return code else: return None - return code From 9f046b58ee83caa4f02bf7f83290013ca4b6bbbf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:27:48 +0000 Subject: [PATCH 063/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.930 → v0.931](https://github.com/pre-commit/mirrors-mypy/compare/v0.930...v0.931) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81622e7..9b6722e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.930 + rev: v0.931 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 1b58293ad3324d248ef8e7af5f4f419f09aec04b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 10 Jan 2022 20:03:25 -0500 Subject: [PATCH 064/257] refactor plugin loading options to prepare for --require-plugins --- src/flake8/main/application.py | 4 +-- src/flake8/plugins/finder.py | 51 ++++++++++++++++++++++--------- tests/integration/test_checker.py | 3 +- tests/integration/test_plugins.py | 7 +++-- tests/unit/plugins/finder_test.py | 23 +++++++++----- 5 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index b9a0e66..97df523 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -119,8 +119,8 @@ class Application: """ raw = finder.find_plugins(cfg) local_plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - enabled = finder.parse_enabled(cfg, enable_extensions) - self.plugins = finder.load_plugins(raw, local_plugin_paths, enabled) + opts = finder.parse_plugin_options(cfg, enable_extensions) + self.plugins = finder.load_plugins(raw, local_plugin_paths, opts) def register_plugin_options(self) -> None: """Register options provided by plugins to our option manager.""" diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 6df0ccc..3157369 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -5,12 +5,12 @@ import logging import sys from typing import Any from typing import Dict +from typing import FrozenSet from typing import Generator from typing import Iterable from typing import List from typing import NamedTuple from typing import Optional -from typing import Set from flake8 import utils from flake8._compat import importlib_metadata @@ -179,22 +179,43 @@ def find_local_plugin_paths( return utils.normalize_paths(paths, cfg_dir) -def parse_enabled( +class PluginOptions(NamedTuple): + """Options related to plugin loading.""" + + enable_extensions: FrozenSet[str] + # TODO: more options here! + # require_plugins: Tuple[str, ...] + + +def _parse_option( cfg: configparser.RawConfigParser, - enable_extensions: Optional[str], -) -> Set[str]: - """Parse --enable-extensions.""" - if enable_extensions is not None: - return set(utils.parse_comma_separated_list(enable_extensions)) + cfg_opt_name: str, + opt: Optional[str], +) -> List[str]: + # specified on commandline: use that + if opt is not None: + return utils.parse_comma_separated_list(opt) else: # ideally this would reuse our config parsing framework but we need to # parse this from preliminary options before plugins are enabled - for opt in ("enable_extensions", "enable-extensions"): - val = cfg.get("flake8", opt, fallback=None) + for opt_name in (cfg_opt_name, cfg_opt_name.replace("_", "-")): + val = cfg.get("flake8", opt_name, fallback=None) if val is not None: - return set(utils.parse_comma_separated_list(val)) + return utils.parse_comma_separated_list(val) else: - return set() + return [] + + +def parse_plugin_options( + cfg: configparser.RawConfigParser, + enable_extensions: Optional[str], +) -> PluginOptions: + """Parse plugin loading related options.""" + return PluginOptions( + enable_extensions=frozenset( + _parse_option(cfg, "enable_extensions", enable_extensions), + ), + ) def _parameters_for(func: Any) -> Dict[str, bool]: @@ -246,7 +267,7 @@ def _import_plugins( def _classify_plugins( plugins: List[LoadedPlugin], - enabled: Set[str], + opts: PluginOptions, ) -> Plugins: tree = [] logical_line = [] @@ -257,7 +278,7 @@ def _classify_plugins( for loaded in plugins: if ( getattr(loaded.obj, "off_by_default", False) - and loaded.plugin.entry_point.name not in enabled + and loaded.plugin.entry_point.name not in opts.enable_extensions ): disabled.append(loaded) elif loaded.plugin.entry_point.group == "flake8.report": @@ -285,7 +306,7 @@ def _classify_plugins( def load_plugins( plugins: List[Plugin], paths: List[str], - enabled: Set[str], + opts: PluginOptions, ) -> Plugins: """Load and classify all flake8 plugins. @@ -293,4 +314,4 @@ def load_plugins( - next: converts the ``Plugin``s to ``LoadedPlugins`` - finally: classifies plugins into their specific types """ - return _classify_plugins(_import_plugins(plugins, paths), enabled) + return _classify_plugins(_import_plugins(plugins, paths), opts) diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 9583179..075b08f 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -90,7 +90,8 @@ def mock_file_checker_with_plugin(plugin_target): ), ), ] - plugins = finder.load_plugins(to_load, [], set()) + opts = finder.PluginOptions(frozenset()) + plugins = finder.load_plugins(to_load, [], opts) # Prevent it from reading lines from stdin or somewhere else with mock.patch( diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index e7f2d38..1085ed1 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -55,7 +55,8 @@ def test_enable_local_plugin_from_config(local_config): cfg, cfg_dir = config.load_config(local_config, [], isolated=False) plugins = finder.find_plugins(cfg) plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - loaded_plugins = finder.load_plugins(plugins, plugin_paths, set()) + opts = finder.PluginOptions(frozenset()) + loaded_plugins = finder.load_plugins(plugins, plugin_paths, opts) (custom_extension,) = ( loaded @@ -82,8 +83,8 @@ def test_local_plugin_can_add_option(local_config): plugins = finder.find_plugins(cfg) plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - enabled = finder.parse_enabled(cfg, stage1_args.enable_extensions) - loaded_plugins = finder.load_plugins(plugins, plugin_paths, enabled) + opts = finder.PluginOptions(frozenset()) + loaded_plugins = finder.load_plugins(plugins, plugin_paths, opts) option_manager = OptionManager( version="123", diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 0e5f81f..330bbf2 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -376,15 +376,18 @@ def test_find_local_plugins(local_plugin_cfg): } -def test_parse_enabled_not_specified(): - assert finder.parse_enabled(configparser.RawConfigParser(), None) == set() +def test_parse_plugin_options_not_specified(): + cfg = configparser.RawConfigParser() + ret = finder.parse_plugin_options(cfg, None) + assert ret == finder.PluginOptions(frozenset()) def test_parse_enabled_from_commandline(): cfg = configparser.RawConfigParser() cfg.add_section("flake8") cfg.set("flake8", "enable_extensions", "A,B,C") - assert finder.parse_enabled(cfg, "D,E,F") == {"D", "E", "F"} + ret = finder.parse_plugin_options(cfg, "D,E,F") + assert ret == finder.PluginOptions(frozenset(("D", "E", "F"))) @pytest.mark.parametrize("opt", ("enable_extensions", "enable-extensions")) @@ -392,7 +395,8 @@ def test_parse_enabled_from_config(opt): cfg = configparser.RawConfigParser() cfg.add_section("flake8") cfg.set("flake8", opt, "A,B,C") - assert finder.parse_enabled(cfg, None) == {"A", "B", "C"} + ret = finder.parse_plugin_options(cfg, None) + assert ret == finder.PluginOptions(frozenset(("A", "B", "C"))) def test_find_plugins( @@ -600,7 +604,7 @@ def test_classify_plugins(): logical_line_plugin, physical_line_plugin, ], - set(), + finder.PluginOptions(frozenset()), ) assert classified == finder.Plugins( @@ -619,8 +623,10 @@ def test_classify_plugins_enable_a_disabled_plugin(): plugin = _plugin(ep=_ep(name="ABC")) loaded = _loaded(plugin=plugin, parameters={"tree": True}, obj=obj) - classified_normal = finder._classify_plugins([loaded], set()) - classified_enabled = finder._classify_plugins([loaded], {"ABC"}) + normal_opts = finder.PluginOptions(frozenset()) + classified_normal = finder._classify_plugins([loaded], normal_opts) + enabled_opts = finder.PluginOptions(frozenset(("ABC",))) + classified_enabled = finder._classify_plugins([loaded], enabled_opts) assert classified_normal == finder.Plugins( checkers=finder.Checkers([], [], []), @@ -638,7 +644,8 @@ def test_classify_plugins_enable_a_disabled_plugin(): def test_load_plugins(): plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) - ret = finder.load_plugins([plugin], ["tests/integration/subdir"], set()) + opts = finder.PluginOptions(frozenset()) + ret = finder.load_plugins([plugin], ["tests/integration/subdir"], opts) import aplugin From c2610debe62c7c8cf25011290dc9dade27dc5c3a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jan 2022 17:20:44 -0500 Subject: [PATCH 065/257] fix docs build --- src/flake8/checker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 2d6421f..d03d84d 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -243,8 +243,7 @@ class Manager: or whether to run them in serial. If running the checks in parallel causes a problem (e.g., - https://github.com/pycqa/flake8/issues/117) this also implements - fallback to serial processing. + :issue:`117`) this also implements fallback to serial processing. """ try: if self.jobs > 1 and len(self.checkers) > 1: From 0bdf9ddf45982b1641f8772a36b1a343eb895b5e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jan 2022 16:03:52 -0500 Subject: [PATCH 066/257] switch from issue templates to issue forms --- .github/ISSUE_TEMPLATE.md | 33 ---------- .github/ISSUE_TEMPLATE/01_bug.yml | 86 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/02_feature.yml | 19 ++++++ .github/ISSUE_TEMPLATE/config.yml | 11 ++++ 4 files changed, 116 insertions(+), 33 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/01_bug.yml create mode 100644 .github/ISSUE_TEMPLATE/02_feature.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 365305d..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,33 +0,0 @@ -Please read this brief portion of documentation before going any further: http://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug - - - - -*Please describe how you installed Flake8* - -Example: - -``` -$ pip install --user flake8 -$ brew install flake8 -# etc. -``` - -**Note**: Some *nix distributions patch Flake8 arbitrarily to accommodate incompatible software versions. If you're on one of those distributions, your issue may be closed and you will be asked to open an issue with your distribution package maintainers instead. - -*Please provide the exact, unmodified output of `flake8 --bug-report`* - -*Please describe the problem or feature* - -*If this is a bug report, please explain with examples (and example code) what you expected to happen and what actually happened.* diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml new file mode 100644 index 0000000..0f95e80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -0,0 +1,86 @@ +name: bug report +description: something went wrong +body: + - type: markdown + attributes: + value: > + Please read this brief portion of documentation before going any + further: + http://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug + + - type: markdown + attributes: + value: > + **NOTE: flake8 is a linting framework and does not implement any + checks** + + - type: markdown + attributes: + value: > + _if you are reporting a problem with a particular check, please track + down the plugin which implements that check_ + + - type: textarea + id: install + attributes: + label: how did you install flake8? + description: 'note: this will be rendered as ```console automatically' + placeholder: | + $ pip install flake8 # or `brew install flake8` etc. + Collecting flake8 + ... + Successfully installed flake8... + render: console + validations: + required: true + + - type: markdown + attributes: + value: > + **Note**: Some *nix distributions patch Flake8 arbitrarily to + accommodate incompatible software versions. If you're on one of those + distributions, your issue may be closed and you will be asked to open + an issue with your distribution package maintainers instead. + + - type: textarea + id: bug-report + attributes: + label: unmodified output of `flake8 --bug-report` + description: 'note: this will be rendered as ```json automatically' + placeholder: | + { + "platform": { + "...": "... + } + } + render: json + validations: + required: true + + - type: textarea + id: freeform + attributes: + label: describe the problem + description: > + please provide **sample code** and **directions for reproducing + your problem** including the **commands you ran**, their + **unedited output**, and **what you expected to happen** + value: | + #### what I expected to happen + + ... + + #### sample code + + ```python + print('hello world!') + ``` + + #### commands ran + + ```console + $ flake8 t.py + ... + ``` + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml new file mode 100644 index 0000000..99cc4e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -0,0 +1,19 @@ +name: feature request +description: a new feature! +body: + - type: markdown + attributes: + value: > + Please read this brief portion of documentation before going any + further: + http://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug + + - type: textarea + id: freeform + attributes: + label: describe the request + description: > + please describe your use case and why the current feature set does + not satisfy your needs + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..44fec6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: problem with E___ or W___ codes + url: https://github.com/PyCQA/pycodestyle/issues + about: flake8 does not implement any checks, perhaps you want pycodestyle? + - name: problem with F___ codes + url: https://github.com/PyCQA/pyflakes/issues + about: flake8 does not implement any checks, perhaps you want pyflakes? + - name: problem with C___ codes + url: https://github.com/PyCQA/mccabe/issues + about: flake8 does not implement any checks, perhaps you want mccabe? From 9b9072e13d5c8ffc1755e9a010ccc505a9a22a51 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Jan 2022 20:25:58 -0500 Subject: [PATCH 067/257] slightly improve unknown parameter error --- src/flake8/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index d03d84d..1161a5b 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -344,7 +344,7 @@ class FileChecker: except AttributeError as ae: LOG.error("Plugin requested unknown parameters.") raise exceptions.PluginRequestedUnknownParameters( - plugin_name=plugin.plugin.package, exception=ae + plugin_name=plugin.display_name, exception=ae ) try: return plugin.obj(**arguments) From c194d6cc30bbb23d6bbbe6a230885626baab2313 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Jan 2022 20:54:23 -0500 Subject: [PATCH 068/257] combine local_plugin_paths and parse_plugin_options --- docs/source/internal/plugin_handling.rst | 4 +- src/flake8/main/application.py | 5 +- src/flake8/plugins/finder.py | 31 +++++----- tests/integration/test_checker.py | 4 +- tests/integration/test_plugins.py | 10 ++-- tests/unit/plugins/finder_test.py | 73 ++++++++++++++---------- 6 files changed, 71 insertions(+), 56 deletions(-) diff --git a/docs/source/internal/plugin_handling.rst b/docs/source/internal/plugin_handling.rst index cdec601..f1c7b9f 100644 --- a/docs/source/internal/plugin_handling.rst +++ b/docs/source/internal/plugin_handling.rst @@ -36,8 +36,8 @@ reporting each check in the ``--version`` output, we only report API Documentation ----------------- +.. autofunction:: flake8.plugins.finder.parse_plugin_options + .. autofunction:: flake8.plugins.finder.find_plugins -.. autofunction:: flake8.plugins.finder.find_local_plugin_paths - .. autofunction:: flake8.plugins.finder.load_plugins diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 97df523..7552528 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -117,10 +117,9 @@ class Application: Set :attr:`plugins` based on loaded plugins. """ + opts = finder.parse_plugin_options(cfg, cfg_dir, enable_extensions) raw = finder.find_plugins(cfg) - local_plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - opts = finder.parse_plugin_options(cfg, enable_extensions) - self.plugins = finder.load_plugins(raw, local_plugin_paths, opts) + self.plugins = finder.load_plugins(raw, opts) def register_plugin_options(self) -> None: """Register options provided by plugins to our option manager.""" diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 3157369..e40b176 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -11,6 +11,7 @@ from typing import Iterable from typing import List from typing import NamedTuple from typing import Optional +from typing import Tuple from flake8 import utils from flake8._compat import importlib_metadata @@ -169,23 +170,19 @@ def find_plugins(cfg: configparser.RawConfigParser) -> List[Plugin]: return ret -def find_local_plugin_paths( - cfg: configparser.RawConfigParser, - cfg_dir: str, -) -> List[str]: - """Discovers the list of ``flake8:local-plugins`` ``paths``.""" - paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() - paths = utils.parse_comma_separated_list(paths_s) - return utils.normalize_paths(paths, cfg_dir) - - class PluginOptions(NamedTuple): """Options related to plugin loading.""" + local_plugin_paths: Tuple[str, ...] enable_extensions: FrozenSet[str] # TODO: more options here! # require_plugins: Tuple[str, ...] + @classmethod + def blank(cls) -> "PluginOptions": + """Make a blank PluginOptions, mostly used for tests.""" + return cls(local_plugin_paths=(), enable_extensions=frozenset()) + def _parse_option( cfg: configparser.RawConfigParser, @@ -208,10 +205,16 @@ def _parse_option( def parse_plugin_options( cfg: configparser.RawConfigParser, + cfg_dir: str, enable_extensions: Optional[str], ) -> PluginOptions: """Parse plugin loading related options.""" + paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() + paths = utils.parse_comma_separated_list(paths_s) + paths = utils.normalize_paths(paths, cfg_dir) + return PluginOptions( + local_plugin_paths=tuple(paths), enable_extensions=frozenset( _parse_option(cfg, "enable_extensions", enable_extensions), ), @@ -259,9 +262,10 @@ def _load_plugin(plugin: Plugin) -> LoadedPlugin: def _import_plugins( - plugins: List[Plugin], paths: List[str] + plugins: List[Plugin], + opts: PluginOptions, ) -> List[LoadedPlugin]: - sys.path.extend(paths) + sys.path.extend(opts.local_plugin_paths) return [_load_plugin(p) for p in plugins] @@ -305,7 +309,6 @@ def _classify_plugins( def load_plugins( plugins: List[Plugin], - paths: List[str], opts: PluginOptions, ) -> Plugins: """Load and classify all flake8 plugins. @@ -314,4 +317,4 @@ def load_plugins( - next: converts the ``Plugin``s to ``LoadedPlugins`` - finally: classifies plugins into their specific types """ - return _classify_plugins(_import_plugins(plugins, paths), opts) + return _classify_plugins(_import_plugins(plugins, opts), opts) diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 075b08f..9223ec4 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -90,8 +90,8 @@ def mock_file_checker_with_plugin(plugin_target): ), ), ] - opts = finder.PluginOptions(frozenset()) - plugins = finder.load_plugins(to_load, [], opts) + opts = finder.PluginOptions.blank() + plugins = finder.load_plugins(to_load, opts) # Prevent it from reading lines from stdin or somewhere else with mock.patch( diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 1085ed1..0950b6c 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -53,10 +53,9 @@ report = def test_enable_local_plugin_from_config(local_config): """App can load a local plugin from config file.""" cfg, cfg_dir = config.load_config(local_config, [], isolated=False) + opts = finder.parse_plugin_options(cfg, cfg_dir, None) plugins = finder.find_plugins(cfg) - plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - opts = finder.PluginOptions(frozenset()) - loaded_plugins = finder.load_plugins(plugins, plugin_paths, opts) + loaded_plugins = finder.load_plugins(plugins, opts) (custom_extension,) = ( loaded @@ -81,10 +80,9 @@ def test_local_plugin_can_add_option(local_config): config=stage1_args.config, extra=[], isolated=False ) + opts = finder.parse_plugin_options(cfg, cfg_dir, None) plugins = finder.find_plugins(cfg) - plugin_paths = finder.find_local_plugin_paths(cfg, cfg_dir) - opts = finder.PluginOptions(frozenset()) - loaded_plugins = finder.load_plugins(plugins, plugin_paths, opts) + loaded_plugins = finder.load_plugins(plugins, opts) option_manager = OptionManager( version="123", diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 330bbf2..d87a77c 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -376,27 +376,43 @@ def test_find_local_plugins(local_plugin_cfg): } -def test_parse_plugin_options_not_specified(): +def test_parse_plugin_options_not_specified(tmp_path): cfg = configparser.RawConfigParser() - ret = finder.parse_plugin_options(cfg, None) - assert ret == finder.PluginOptions(frozenset()) + ret = finder.parse_plugin_options(cfg, str(tmp_path), None) + assert ret == finder.PluginOptions((), frozenset()) -def test_parse_enabled_from_commandline(): +def test_parse_enabled_from_commandline(tmp_path): cfg = configparser.RawConfigParser() cfg.add_section("flake8") cfg.set("flake8", "enable_extensions", "A,B,C") - ret = finder.parse_plugin_options(cfg, "D,E,F") - assert ret == finder.PluginOptions(frozenset(("D", "E", "F"))) + ret = finder.parse_plugin_options(cfg, str(tmp_path), "D,E,F") + assert ret == finder.PluginOptions((), frozenset(("D", "E", "F"))) @pytest.mark.parametrize("opt", ("enable_extensions", "enable-extensions")) -def test_parse_enabled_from_config(opt): +def test_parse_enabled_from_config(opt, tmp_path): cfg = configparser.RawConfigParser() cfg.add_section("flake8") cfg.set("flake8", opt, "A,B,C") - ret = finder.parse_plugin_options(cfg, None) - assert ret == finder.PluginOptions(frozenset(("A", "B", "C"))) + ret = finder.parse_plugin_options(cfg, str(tmp_path), None) + assert ret == finder.PluginOptions((), frozenset(("A", "B", "C"))) + + +def test_parse_plugin_options_local_plugin_paths_missing(tmp_path): + cfg = configparser.RawConfigParser() + opts = finder.parse_plugin_options(cfg, str(tmp_path), None) + assert opts.local_plugin_paths == () + + +def test_parse_plugin_options_local_plugin_paths(tmp_path): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8:local-plugins") + cfg.set("flake8:local-plugins", "paths", "./a, ./b") + opts = finder.parse_plugin_options(cfg, str(tmp_path), None) + + expected = (str(tmp_path.joinpath("a")), str(tmp_path.joinpath("b"))) + assert opts.local_plugin_paths == expected def test_find_plugins( @@ -489,20 +505,6 @@ def test_find_plugins( ] -def test_find_local_plugin_paths_missing(tmp_path): - cfg = configparser.RawConfigParser() - assert finder.find_local_plugin_paths(cfg, str(tmp_path)) == [] - - -def test_find_local_plugin_paths(tmp_path): - cfg = configparser.RawConfigParser() - cfg.add_section("flake8:local-plugins") - cfg.set("flake8:local-plugins", "paths", "./a, ./b") - ret = finder.find_local_plugin_paths(cfg, str(tmp_path)) - - assert ret == [str(tmp_path.joinpath("a")), str(tmp_path.joinpath("b"))] - - def test_parameters_for_class_plugin(): """Verify that we can retrieve the parameters for a class plugin.""" @@ -576,7 +578,11 @@ def reset_sys(): def test_import_plugins_extends_sys_path(): plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) - ret = finder._import_plugins([plugin], ["tests/integration/subdir"]) + opts = finder.PluginOptions( + local_plugin_paths=("tests/integration/subdir",), + enable_extensions=frozenset(), + ) + ret = finder._import_plugins([plugin], opts) import aplugin @@ -604,7 +610,7 @@ def test_classify_plugins(): logical_line_plugin, physical_line_plugin, ], - finder.PluginOptions(frozenset()), + finder.PluginOptions.blank(), ) assert classified == finder.Plugins( @@ -623,9 +629,15 @@ def test_classify_plugins_enable_a_disabled_plugin(): plugin = _plugin(ep=_ep(name="ABC")) loaded = _loaded(plugin=plugin, parameters={"tree": True}, obj=obj) - normal_opts = finder.PluginOptions(frozenset()) + normal_opts = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + ) classified_normal = finder._classify_plugins([loaded], normal_opts) - enabled_opts = finder.PluginOptions(frozenset(("ABC",))) + enabled_opts = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(("ABC",)), + ) classified_enabled = finder._classify_plugins([loaded], enabled_opts) assert classified_normal == finder.Plugins( @@ -644,8 +656,11 @@ def test_classify_plugins_enable_a_disabled_plugin(): def test_load_plugins(): plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) - opts = finder.PluginOptions(frozenset()) - ret = finder.load_plugins([plugin], ["tests/integration/subdir"], opts) + opts = finder.PluginOptions( + local_plugin_paths=("tests/integration/subdir",), + enable_extensions=frozenset(), + ) + ret = finder.load_plugins([plugin], opts) import aplugin From ec975ad6b2d80ae828930d215daa7b5f3b4681cd Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Wed, 19 Jan 2022 07:56:03 +0100 Subject: [PATCH 069/257] link to correct paragraph in feature-request issue template --- .github/ISSUE_TEMPLATE/01_bug.yml | 2 +- .github/ISSUE_TEMPLATE/02_feature.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml index 0f95e80..3005b8b 100644 --- a/.github/ISSUE_TEMPLATE/01_bug.yml +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -6,7 +6,7 @@ body: value: > Please read this brief portion of documentation before going any further: - http://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug + https://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml index 99cc4e2..f398384 100644 --- a/.github/ISSUE_TEMPLATE/02_feature.yml +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -6,7 +6,7 @@ body: value: > Please read this brief portion of documentation before going any further: - http://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug + https://flake8.pycqa.org/en/latest/internal/contributing.html#requesting-a-new-feature - type: textarea id: freeform From 9f2331cb0c94fc737efdcb56f8135a89c42e7bfe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Jan 2022 05:54:42 -0800 Subject: [PATCH 070/257] update feature issue form - revert link back to the top level header - add notices about not implementing any checks --- .github/ISSUE_TEMPLATE/02_feature.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml index f398384..d6ed1a9 100644 --- a/.github/ISSUE_TEMPLATE/02_feature.yml +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -6,7 +6,11 @@ body: value: > Please read this brief portion of documentation before going any further: - https://flake8.pycqa.org/en/latest/internal/contributing.html#requesting-a-new-feature + https://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug + + - type: markdown + attributes: + value: **NOTE: flake8 is a linting framework and does not implement any checks** - type: textarea id: freeform From 61e9d72b77fa8417637cfbc77d7a40de43a3d92f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Jan 2022 09:39:13 -0500 Subject: [PATCH 071/257] fix yaml quoting issue --- .github/ISSUE_TEMPLATE/02_feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml index d6ed1a9..61fcf25 100644 --- a/.github/ISSUE_TEMPLATE/02_feature.yml +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -10,7 +10,7 @@ body: - type: markdown attributes: - value: **NOTE: flake8 is a linting framework and does not implement any checks** + value: '**NOTE: flake8 is a linting framework and does not implement any checks**' - type: textarea id: freeform From 9d23faad6d934809ce42b0ce925d65c1b1a92091 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 20 Jan 2022 13:56:34 -0500 Subject: [PATCH 072/257] always use UTF-8 encoding when reading configuration --- src/flake8/options/config.py | 6 +++--- tests/unit/test_options_config.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index 7cba936..9756e7d 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -20,7 +20,7 @@ def _find_config_file(path: str) -> Optional[str]: for candidate in ("setup.cfg", "tox.ini", ".flake8"): cfg_path = os.path.join(path, candidate) try: - cfg.read(cfg_path) + cfg.read(cfg_path, encoding="UTF-8") except (UnicodeDecodeError, configparser.ParsingError) as e: LOG.warning("ignoring unparseable config %s: %s", cfg_path, e) else: @@ -61,7 +61,7 @@ def load_config( cfg = configparser.RawConfigParser() if config is not None: - if not cfg.read(config): + if not cfg.read(config, encoding="UTF-8"): raise exceptions.ExecutionError( f"The specified config file does not exist: {config}" ) @@ -72,7 +72,7 @@ def load_config( # TODO: remove this and replace it with configuration modifying plugins # read the additional configs afterwards for filename in extra: - cfg.read(filename) + cfg.read(filename, encoding="UTF-8") return cfg, cfg_dir diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index c5d1476..02dd218 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -118,6 +118,31 @@ def test_load_config_append_config(tmpdir): assert cfg_dir == str(tmpdir) +NON_ASCII_CONFIG = "# ☃\n[flake8]\nindent-size=8\n" + + +def test_load_auto_config_utf8(tmpdir): + tmpdir.join("setup.cfg").write_text(NON_ASCII_CONFIG, encoding="UTF-8") + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + assert cfg["flake8"]["indent-size"] == "8" + + +def test_load_explicit_config_utf8(tmpdir): + tmpdir.join("t.cfg").write_text(NON_ASCII_CONFIG, encoding="UTF-8") + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config("t.cfg", [], isolated=False) + assert cfg["flake8"]["indent-size"] == "8" + + +def test_load_extra_config_utf8(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + tmpdir.join("t.cfg").write_text(NON_ASCII_CONFIG, encoding="UTF-8") + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, ["t.cfg"], isolated=False) + assert cfg["flake8"]["indent-size"] == "8" + + @pytest.fixture def opt_manager(): ret = OptionManager(version="123", plugin_versions="", parents=[]) From d03b9c97cc34406f9329510e9359cec15ba66b36 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Jan 2022 13:58:46 -0500 Subject: [PATCH 073/257] add a --require-plugins option --- docs/source/user/options.rst | 32 ++++++- src/flake8/api/legacy.py | 7 +- src/flake8/main/application.py | 18 +++- src/flake8/main/options.py | 5 ++ src/flake8/plugins/finder.py | 137 ++++++++++++++++++------------ src/flake8/utils.py | 6 ++ tests/integration/test_plugins.py | 18 +++- tests/unit/plugins/finder_test.py | 128 ++++++++++++++++++++++++++-- tests/unit/test_legacy_api.py | 8 +- tests/unit/test_utils.py | 16 ++++ 10 files changed, 303 insertions(+), 72 deletions(-) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index 8be702c..8bf7e33 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -78,6 +78,8 @@ Index of Options - :option:`flake8 --statistics` +- :option:`flake8 --require-plugins` + - :option:`flake8 --enable-extensions` - :option:`flake8 --exit-zero` @@ -772,6 +774,32 @@ Options and their Descriptions statistics = True +.. option:: --require-plugins= + + :ref:`Go back to index ` + + Require specific plugins to be installed before running. + + This option takes a list of distribution names (usually the name you would + use when running ``pip install``). + + Command-line example: + + .. prompt:: bash + + flake8 --require-plugins=flake8-2020,flake8-typing-extensions dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + require-plugins = + flake8-2020 + flake8-typing-extensions + + .. option:: --enable-extensions= :ref:`Go back to index ` @@ -779,8 +807,8 @@ Options and their Descriptions Enable off-by-default extensions. Plugins to |Flake8| have the option of registering themselves as - off-by-default. These plugins effectively add themselves to the - default ignore list. + off-by-default. These plugins will not be loaded unless enabled by this + option. Command-line example: diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 089543d..8fb7ab6 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -212,7 +212,12 @@ def get_style_guide(**kwargs: Any) -> StyleGuide: isolated=prelim_opts.isolated, ) - application.find_plugins(cfg, cfg_dir, prelim_opts.enable_extensions) + application.find_plugins( + cfg, + cfg_dir, + enable_extensions=prelim_opts.enable_extensions, + require_plugins=prelim_opts.require_plugins, + ) application.register_plugin_options() application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) # We basically want application.initialize to be called but with these diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 7552528..df32f92 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -111,14 +111,21 @@ class Application: self, cfg: configparser.RawConfigParser, cfg_dir: str, + *, enable_extensions: Optional[str], + require_plugins: Optional[str], ) -> None: """Find and load the plugins for this application. Set :attr:`plugins` based on loaded plugins. """ - opts = finder.parse_plugin_options(cfg, cfg_dir, enable_extensions) - raw = finder.find_plugins(cfg) + opts = finder.parse_plugin_options( + cfg, + cfg_dir, + enable_extensions=enable_extensions, + require_plugins=require_plugins, + ) + raw = finder.find_plugins(cfg, opts) self.plugins = finder.load_plugins(raw, opts) def register_plugin_options(self) -> None: @@ -295,7 +302,12 @@ class Application: isolated=prelim_opts.isolated, ) - self.find_plugins(cfg, cfg_dir, prelim_opts.enable_extensions) + self.find_plugins( + cfg, + cfg_dir, + enable_extensions=prelim_opts.enable_extensions, + require_plugins=prelim_opts.require_plugins, + ) self.register_plugin_options() self.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) self.make_formatter() diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 201483b..cb4cd73 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -68,6 +68,11 @@ def stage1_arg_parser() -> argparse.ArgumentParser: "by default", ) + parser.add_argument( + "--require-plugins", + help="Require specific plugins to be installed before running", + ) + return parser diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index e40b176..43b5417 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -15,6 +15,7 @@ from typing import Tuple from flake8 import utils from flake8._compat import importlib_metadata +from flake8.exceptions import ExecutionError from flake8.exceptions import FailedToLoadPlugin LOG = logging.getLogger(__name__) @@ -88,6 +89,65 @@ class Plugins(NamedTuple): ) +class PluginOptions(NamedTuple): + """Options related to plugin loading.""" + + local_plugin_paths: Tuple[str, ...] + enable_extensions: FrozenSet[str] + require_plugins: FrozenSet[str] + + @classmethod + def blank(cls) -> "PluginOptions": + """Make a blank PluginOptions, mostly used for tests.""" + return cls( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + + +def _parse_option( + cfg: configparser.RawConfigParser, + cfg_opt_name: str, + opt: Optional[str], +) -> List[str]: + # specified on commandline: use that + if opt is not None: + return utils.parse_comma_separated_list(opt) + else: + # ideally this would reuse our config parsing framework but we need to + # parse this from preliminary options before plugins are enabled + for opt_name in (cfg_opt_name, cfg_opt_name.replace("_", "-")): + val = cfg.get("flake8", opt_name, fallback=None) + if val is not None: + return utils.parse_comma_separated_list(val) + else: + return [] + + +def parse_plugin_options( + cfg: configparser.RawConfigParser, + cfg_dir: str, + *, + enable_extensions: Optional[str], + require_plugins: Optional[str], +) -> PluginOptions: + """Parse plugin loading related options.""" + paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() + paths = utils.parse_comma_separated_list(paths_s) + paths = utils.normalize_paths(paths, cfg_dir) + + return PluginOptions( + local_plugin_paths=tuple(paths), + enable_extensions=frozenset( + _parse_option(cfg, "enable_extensions", enable_extensions), + ), + require_plugins=frozenset( + _parse_option(cfg, "require_plugins", require_plugins), + ), + ) + + def _flake8_plugins( eps: Iterable[importlib_metadata.EntryPoint], name: str, @@ -160,67 +220,40 @@ def _find_local_plugins( yield Plugin("local", "local", ep) -def find_plugins(cfg: configparser.RawConfigParser) -> List[Plugin]: +def _check_required_plugins( + plugins: List[Plugin], + expected: FrozenSet[str], +) -> None: + plugin_names = { + utils.normalize_pypi_name(plugin.package) for plugin in plugins + } + expected_names = {utils.normalize_pypi_name(name) for name in expected} + missing_plugins = expected_names - plugin_names + + if missing_plugins: + raise ExecutionError( + f"required plugins were not installed!\n" + f"- installed: {', '.join(sorted(plugin_names))}\n" + f"- expected: {', '.join(sorted(expected_names))}\n" + f"- missing: {', '.join(sorted(missing_plugins))}" + ) + + +def find_plugins( + cfg: configparser.RawConfigParser, + opts: PluginOptions, +) -> List[Plugin]: """Discovers all plugins (but does not load them).""" ret = [*_find_importlib_plugins(), *_find_local_plugins(cfg)] # for determinism, sort the list ret.sort() + _check_required_plugins(ret, opts.require_plugins) + return ret -class PluginOptions(NamedTuple): - """Options related to plugin loading.""" - - local_plugin_paths: Tuple[str, ...] - enable_extensions: FrozenSet[str] - # TODO: more options here! - # require_plugins: Tuple[str, ...] - - @classmethod - def blank(cls) -> "PluginOptions": - """Make a blank PluginOptions, mostly used for tests.""" - return cls(local_plugin_paths=(), enable_extensions=frozenset()) - - -def _parse_option( - cfg: configparser.RawConfigParser, - cfg_opt_name: str, - opt: Optional[str], -) -> List[str]: - # specified on commandline: use that - if opt is not None: - return utils.parse_comma_separated_list(opt) - else: - # ideally this would reuse our config parsing framework but we need to - # parse this from preliminary options before plugins are enabled - for opt_name in (cfg_opt_name, cfg_opt_name.replace("_", "-")): - val = cfg.get("flake8", opt_name, fallback=None) - if val is not None: - return utils.parse_comma_separated_list(val) - else: - return [] - - -def parse_plugin_options( - cfg: configparser.RawConfigParser, - cfg_dir: str, - enable_extensions: Optional[str], -) -> PluginOptions: - """Parse plugin loading related options.""" - paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() - paths = utils.parse_comma_separated_list(paths_s) - paths = utils.normalize_paths(paths, cfg_dir) - - return PluginOptions( - local_plugin_paths=tuple(paths), - enable_extensions=frozenset( - _parse_option(cfg, "enable_extensions", enable_extensions), - ), - ) - - def _parameters_for(func: Any) -> Dict[str, bool]: """Return the parameters for the plugin. diff --git a/src/flake8/utils.py b/src/flake8/utils.py index 71f9dd0..cc47ffc 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -25,6 +25,7 @@ from flake8 import exceptions 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]") +NORMALIZE_PACKAGE_NAME_RE = re.compile(r"[-_.]+") def parse_comma_separated_list( @@ -343,3 +344,8 @@ def get_python_version() -> str: platform.python_version(), platform.system(), ) + + +def normalize_pypi_name(s: str) -> str: + """Normalize a distribution name according to PEP 503.""" + return NORMALIZE_PACKAGE_NAME_RE.sub("-", s).lower() diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 0950b6c..c3be049 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -53,8 +53,13 @@ report = def test_enable_local_plugin_from_config(local_config): """App can load a local plugin from config file.""" cfg, cfg_dir = config.load_config(local_config, [], isolated=False) - opts = finder.parse_plugin_options(cfg, cfg_dir, None) - plugins = finder.find_plugins(cfg) + opts = finder.parse_plugin_options( + cfg, + cfg_dir, + enable_extensions=None, + require_plugins=None, + ) + plugins = finder.find_plugins(cfg, opts) loaded_plugins = finder.load_plugins(plugins, opts) (custom_extension,) = ( @@ -80,8 +85,13 @@ def test_local_plugin_can_add_option(local_config): config=stage1_args.config, extra=[], isolated=False ) - opts = finder.parse_plugin_options(cfg, cfg_dir, None) - plugins = finder.find_plugins(cfg) + opts = finder.parse_plugin_options( + cfg, + cfg_dir, + enable_extensions=None, + require_plugins=None, + ) + plugins = finder.find_plugins(cfg, opts) loaded_plugins = finder.load_plugins(plugins, opts) option_manager = OptionManager( diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index d87a77c..15f15a2 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -5,6 +5,7 @@ from unittest import mock import pytest from flake8._compat import importlib_metadata +from flake8.exceptions import ExecutionError from flake8.exceptions import FailedToLoadPlugin from flake8.plugins import finder from flake8.plugins.pyflakes import FlakesChecker @@ -378,16 +379,31 @@ def test_find_local_plugins(local_plugin_cfg): def test_parse_plugin_options_not_specified(tmp_path): cfg = configparser.RawConfigParser() - ret = finder.parse_plugin_options(cfg, str(tmp_path), None) - assert ret == finder.PluginOptions((), frozenset()) + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) + expected = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + assert opts == expected def test_parse_enabled_from_commandline(tmp_path): cfg = configparser.RawConfigParser() cfg.add_section("flake8") cfg.set("flake8", "enable_extensions", "A,B,C") - ret = finder.parse_plugin_options(cfg, str(tmp_path), "D,E,F") - assert ret == finder.PluginOptions((), frozenset(("D", "E", "F"))) + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions="D,E,F", + require_plugins=None, + ) + assert opts.enable_extensions == frozenset(("D", "E", "F")) @pytest.mark.parametrize("opt", ("enable_extensions", "enable-extensions")) @@ -395,13 +411,23 @@ def test_parse_enabled_from_config(opt, tmp_path): cfg = configparser.RawConfigParser() cfg.add_section("flake8") cfg.set("flake8", opt, "A,B,C") - ret = finder.parse_plugin_options(cfg, str(tmp_path), None) - assert ret == finder.PluginOptions((), frozenset(("A", "B", "C"))) + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) + assert opts.enable_extensions == frozenset(("A", "B", "C")) def test_parse_plugin_options_local_plugin_paths_missing(tmp_path): cfg = configparser.RawConfigParser() - opts = finder.parse_plugin_options(cfg, str(tmp_path), None) + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) assert opts.local_plugin_paths == () @@ -409,7 +435,12 @@ def test_parse_plugin_options_local_plugin_paths(tmp_path): cfg = configparser.RawConfigParser() cfg.add_section("flake8:local-plugins") cfg.set("flake8:local-plugins", "paths", "./a, ./b") - opts = finder.parse_plugin_options(cfg, str(tmp_path), None) + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) expected = (str(tmp_path.joinpath("a")), str(tmp_path.joinpath("b"))) assert opts.local_plugin_paths == expected @@ -422,12 +453,13 @@ def test_find_plugins( mock_distribution, local_plugin_cfg, ): + opts = finder.PluginOptions.blank() with mock.patch.object( importlib_metadata, "distributions", return_value=[flake8_dist, flake8_foo_dist], ): - ret = finder.find_plugins(local_plugin_cfg) + ret = finder.find_plugins(local_plugin_cfg, opts) assert ret == [ finder.Plugin( @@ -505,6 +537,80 @@ def test_find_plugins( ] +def test_find_plugins_plugin_is_present(flake8_foo_dist): + cfg = configparser.RawConfigParser() + options_flake8_foo_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(("flake8-foo",)), + ) + options_not_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + + with mock.patch.object( + importlib_metadata, + "distributions", + return_value=[flake8_foo_dist], + ): + # neither of these raise, `flake8-foo` is satisfied + finder.find_plugins(cfg, options_flake8_foo_required) + finder.find_plugins(cfg, options_not_required) + + +def test_find_plugins_plugin_is_missing(flake8_dist, flake8_foo_dist): + cfg = configparser.RawConfigParser() + options_flake8_foo_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(("flake8-foo",)), + ) + options_not_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + + with mock.patch.object( + importlib_metadata, + "distributions", + return_value=[flake8_dist], + ): + # this is ok, no special requirements + finder.find_plugins(cfg, options_not_required) + + # but we get a nice error for missing plugins here! + with pytest.raises(ExecutionError) as excinfo: + finder.find_plugins(cfg, options_flake8_foo_required) + + (msg,) = excinfo.value.args + assert msg == ( + "required plugins were not installed!\n" + "- installed: flake8, pycodestyle, pyflakes\n" + "- expected: flake8-foo\n" + "- missing: flake8-foo" + ) + + +def test_find_plugins_name_normalization(flake8_foo_dist): + cfg = configparser.RawConfigParser() + opts = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + # this name will be normalized before checking + require_plugins=frozenset(("Flake8_Foo",)), + ) + + with mock.patch.object( + importlib_metadata, + "distributions", + return_value=[flake8_foo_dist], + ): + finder.find_plugins(cfg, opts) + + def test_parameters_for_class_plugin(): """Verify that we can retrieve the parameters for a class plugin.""" @@ -581,6 +687,7 @@ def test_import_plugins_extends_sys_path(): opts = finder.PluginOptions( local_plugin_paths=("tests/integration/subdir",), enable_extensions=frozenset(), + require_plugins=frozenset(), ) ret = finder._import_plugins([plugin], opts) @@ -632,11 +739,13 @@ def test_classify_plugins_enable_a_disabled_plugin(): normal_opts = finder.PluginOptions( local_plugin_paths=(), enable_extensions=frozenset(), + require_plugins=frozenset(), ) classified_normal = finder._classify_plugins([loaded], normal_opts) enabled_opts = finder.PluginOptions( local_plugin_paths=(), enable_extensions=frozenset(("ABC",)), + require_plugins=frozenset(), ) classified_enabled = finder._classify_plugins([loaded], enabled_opts) @@ -659,6 +768,7 @@ def test_load_plugins(): opts = finder.PluginOptions( local_plugin_paths=("tests/integration/subdir",), enable_extensions=frozenset(), + require_plugins=frozenset(), ) ret = finder.load_plugins([plugin], opts) diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 4b44fb8..169969b 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -20,6 +20,7 @@ def test_get_style_guide(): output_file=None, verbose=0, enable_extensions=None, + require_plugins=None, ) mockedapp = mock.Mock() mockedapp.parse_preliminary_options.return_value = (prelim_opts, []) @@ -35,7 +36,12 @@ def test_get_style_guide(): application.assert_called_once_with() mockedapp.parse_preliminary_options.assert_called_once_with([]) - mockedapp.find_plugins.assert_called_once_with(cfg, cfg_dir, None) + mockedapp.find_plugins.assert_called_once_with( + cfg, + cfg_dir, + enable_extensions=None, + require_plugins=None, + ) mockedapp.register_plugin_options.assert_called_once_with() mockedapp.parse_configuration_and_cli.assert_called_once_with( cfg, cfg_dir, [] diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 5aadf2f..fefe662 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -231,3 +231,19 @@ def test_stdin_unknown_coding_token(): stdin = io.TextIOWrapper(io.BytesIO(b"# coding: unknown\n"), "UTF-8") with mock.patch.object(sys, "stdin", stdin): assert utils.stdin_get_value.__wrapped__() == "# coding: unknown\n" + + +@pytest.mark.parametrize( + ("s", "expected"), + ( + ("", ""), + ("my-plugin", "my-plugin"), + ("MyPlugin", "myplugin"), + ("my_plugin", "my-plugin"), + ("my.plugin", "my-plugin"), + ("my--plugin", "my-plugin"), + ("my__plugin", "my-plugin"), + ), +) +def test_normalize_pypi_name(s, expected): + assert utils.normalize_pypi_name(s) == expected From 23a60dd9028f35c9f0bb95913cc9dc639b1d43c3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Jan 2022 15:17:10 -0500 Subject: [PATCH 074/257] use the actual line contents when processing physical lines --- src/flake8/checker.py | 2 +- src/flake8/processor.py | 6 ++++-- tests/integration/test_plugins.py | 34 +++++++++++++++++++++++++++++ tests/unit/test_file_processor.py | 36 +++++++++++++++---------------- 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 1161a5b..651cd34 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -581,7 +581,7 @@ class FileChecker: line_no = token[2][0] with self.processor.inside_multiline(line_number=line_no): for line in self.processor.split_line(token): - self.run_physical_checks(line + "\n") + self.run_physical_checks(line) def _pool_init() -> None: diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 40b3266..bc0ef51 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -233,8 +233,10 @@ class FileProcessor: This also auto-increments the line number for the caller. """ - for line in token[1].split("\n")[:-1]: - yield line + # intentionally don't include the last line, that line will be + # terminated later by a future end-of-line + for line_no in range(token.start[0], token.end[0]): + yield self.lines[line_no - 1] self.line_number += 1 def keyword_arguments_for( diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index c3be049..03f0caf 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -160,3 +160,37 @@ extension = out, err = capsys.readouterr() assert out == f"{t_py}:1:1: ABC123 error\n" assert err == "" + + +def yields_physical_line(physical_line): + yield 0, f"T001 {physical_line!r}" + + +def test_physical_line_plugin_multiline_string(tmpdir, capsys): + cfg_s = f"""\ +[flake8:local-plugins] +extension = + T = {yields_physical_line.__module__}:{yields_physical_line.__name__} +""" + + cfg = tmpdir.join("tox.ini") + cfg.write(cfg_s) + + src = '''\ +x = "foo" + """ +bar +""" +''' + t_py = tmpdir.join("t.py") + t_py.write_binary(src.encode()) + + with tmpdir.as_cwd(): + assert main(("t.py", "--config", str(cfg))) == 1 + + expected = '''\ +t.py:1:1: T001 'x = "foo" + """\\n' +t.py:2:1: T001 'bar\\n' +t.py:3:1: T001 '"""\\n' +''' + out, err = capsys.readouterr() + assert out == expected diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index 78740fc..c5c10d5 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -256,30 +256,30 @@ def test_keyword_arguments_for_does_not_handle_attribute_errors( file_processor.keyword_arguments_for({"fake": True}) -@pytest.mark.parametrize( - "unsplit_line, expected_lines", - [ - ("line", []), - ("line 1\n", ["line 1"]), - ("line 1\nline 2\n", ["line 1", "line 2"]), - ("line 1\n\nline 2\n", ["line 1", "", "line 2"]), - ], -) -def test_split_line(unsplit_line, expected_lines, default_options): - """Verify the token line splitting.""" +def test_processor_split_line(default_options): file_processor = processor.FileProcessor( "-", default_options, lines=[ - "Line 1", + 'x = """\n', + "contents\n", + '"""\n', ], ) - - token = tokenize.TokenInfo(1, unsplit_line, (0, 0), (0, 0), "") - actual_lines = list(file_processor.split_line(token)) - assert expected_lines == actual_lines - - assert len(actual_lines) == file_processor.line_number + token = tokenize.TokenInfo( + 3, + '"""\ncontents\n"""', + (1, 4), + (3, 3), + 'x = """\ncontents\n"""\n', + ) + expected = [('x = """\n', 0), ("contents\n", 1)] + actual = [ + (line, file_processor.line_number) + for line in file_processor.split_line(token) + ] + assert expected == actual + assert file_processor.line_number == 2 def test_build_ast(default_options): From 929cf5dfd3cf269e4eff1b46dd28bd19d15bcdee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 18:02:39 -0500 Subject: [PATCH 075/257] remove `log_token` and `EXTRA_VERBOSE` - flake8 spent 5% of execution in `log_token` - `EXTRA_VERBOSE` was only used by `log_token` - `python -m tokenize` provides better debug token output --- docs/source/internal/checker.rst | 2 -- src/flake8/__init__.py | 11 +------- src/flake8/checker.py | 1 - src/flake8/processor.py | 14 ---------- tests/unit/conftest.py | 2 +- tests/unit/test_file_processor.py | 45 ------------------------------- 6 files changed, 2 insertions(+), 73 deletions(-) diff --git a/docs/source/internal/checker.rst b/docs/source/internal/checker.rst index 35eac0e..fd571a7 100644 --- a/docs/source/internal/checker.rst +++ b/docs/source/internal/checker.rst @@ -57,8 +57,6 @@ Utility Functions .. autofunction:: flake8.processor.is_multiline_string -.. autofunction:: flake8.processor.log_token - .. autofunction:: flake8.processor.mutate_string .. autofunction:: flake8.processor.token_is_newline diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 47523d0..aef6354 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -20,19 +20,11 @@ LOG.addHandler(logging.NullHandler()) __version__ = "4.0.1" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) - -# There is nothing lower than logging.DEBUG (10) in the logging library, -# but we want an extra level to avoid being too verbose when using -vv. -_EXTRA_VERBOSE = 5 -logging.addLevelName(_EXTRA_VERBOSE, "VERBOSE") - _VERBOSITY_TO_LOG_LEVEL = { # output more than warnings but not debugging info 1: logging.INFO, # INFO is a numerical level of 20 # output debugging information 2: logging.DEBUG, # DEBUG is a numerical level of 10 - # output extra verbose debugging information - 3: _EXTRA_VERBOSE, } LOG_FORMAT = ( @@ -58,9 +50,8 @@ def configure_logging( """ if verbosity <= 0: return - if verbosity > 3: - verbosity = 3 + verbosity = min(verbosity, max(_VERBOSITY_TO_LOG_LEVEL)) log_level = _VERBOSITY_TO_LOG_LEVEL[verbosity] if not filename or filename in ("stderr", "stdout"): diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 651cd34..db13eb1 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -510,7 +510,6 @@ class FileChecker: statistics["tokens"] += 1 self.check_physical_eol(token, prev_physical) token_type, text = token[0:2] - processor.log_token(LOG, token) if token_type == tokenize.OP: parens = processor.count_parentheses(parens, text) elif parens == 0: diff --git a/src/flake8/processor.py b/src/flake8/processor.py index bc0ef51..8589a53 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -11,7 +11,6 @@ from typing import List from typing import Optional from typing import Tuple -import flake8 from flake8 import defaults from flake8 import utils from flake8.plugins.finder import LoadedPlugin @@ -412,19 +411,6 @@ def count_parentheses(current_parentheses_count: int, token_text: str) -> int: return current_parentheses_count -def log_token(log: logging.Logger, token: tokenize.TokenInfo) -> None: - """Log a token to a provided logging object.""" - if token[2][0] == token[3][0]: - pos = "[{}:{}]".format(token[2][1] or "", token[3][1]) - else: - pos = f"l.{token[3][0]}" - log.log( - flake8._EXTRA_VERBOSE, - "l.%s\t%s\t%s\t%r" - % (token[2][0], pos, tokenize.tok_name[token[0]], token[1]), - ) - - def expand_indent(line: str) -> int: r"""Return the amount of indentation. diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 2808387..904366e 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -10,7 +10,7 @@ def options_from(**kwargs): kwargs.setdefault("max_line_length", 79) kwargs.setdefault("max_doc_length", None) kwargs.setdefault("indent_size", 4) - kwargs.setdefault("verbose", False) + kwargs.setdefault("verbose", 0) kwargs.setdefault("stdin_display_name", "stdin") kwargs.setdefault("disable_noqa", False) return argparse.Namespace(**kwargs) diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index c5c10d5..fb6664a 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -380,51 +380,6 @@ def test_expand_indent(string, expected): assert expected == actual -@pytest.mark.parametrize( - "token, log_string", - [ - [ - ( - tokenize.COMMENT, - "# this is a comment", - (1, 0), # (start_row, start_column) - (1, 19), # (end_ro, end_column) - "# this is a comment", - ), - "l.1\t[:19]\tCOMMENT\t'# this is a comment'", - ], - [ - ( - tokenize.COMMENT, - "# this is a comment", - (1, 5), # (start_row, start_column) - (1, 19), # (end_ro, end_column) - "# this is a comment", - ), - "l.1\t[5:19]\tCOMMENT\t'# this is a comment'", - ], - [ - ( - tokenize.COMMENT, - "# this is a comment", - (1, 0), # (start_row, start_column) - (2, 19), # (end_ro, end_column) - "# this is a comment", - ), - "l.1\tl.2\tCOMMENT\t'# this is a comment'", - ], - ], -) -def test_log_token(token, log_string): - """Verify we use the log object passed in.""" - log = mock.Mock() - processor.log_token(log, token) - log.log.assert_called_once_with( - 5, # flake8._EXTRA_VERBOSE - log_string, - ) - - @pytest.mark.parametrize( "current_count, token_text, expected", [ From 9343a993f616d3062de088dc3171a6d2ad4b4117 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 18:32:28 -0500 Subject: [PATCH 076/257] remove triple logged stacktrace on unknown plugin args --- src/flake8/checker.py | 1 - src/flake8/processor.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index db13eb1..2796aec 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -342,7 +342,6 @@ class FileChecker: try: self.processor.keyword_arguments_for(plugin.parameters, arguments) except AttributeError as ae: - LOG.error("Plugin requested unknown parameters.") raise exceptions.PluginRequestedUnknownParameters( plugin_name=plugin.display_name, exception=ae ) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 8589a53..22c8f44 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -251,9 +251,8 @@ class FileProcessor: continue try: arguments[param] = getattr(self, param) - except AttributeError as exc: + except AttributeError: if required: - LOG.exception(exc) raise else: LOG.warning( From f9eb0fd6eaa3b2b7c01e77c11392218a418123a5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 19:06:06 -0500 Subject: [PATCH 077/257] change keyword_arguments_for so it does not modify and return --- src/flake8/checker.py | 6 ++++-- src/flake8/processor.py | 9 ++++----- tests/unit/test_file_processor.py | 16 ++++++++-------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 2796aec..2e8117f 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -340,13 +340,15 @@ class FileChecker: LOG.debug("Running %r with %r", plugin, arguments) assert self.processor is not None try: - self.processor.keyword_arguments_for(plugin.parameters, arguments) + params = self.processor.keyword_arguments_for( + plugin.parameters, arguments + ) except AttributeError as ae: raise exceptions.PluginRequestedUnknownParameters( plugin_name=plugin.display_name, exception=ae ) try: - return plugin.obj(**arguments) + return plugin.obj(**arguments, **params) except Exception as all_exc: LOG.critical( "Plugin %s raised an unexpected exception", diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 22c8f44..fa9bd2f 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -241,16 +241,15 @@ class FileProcessor: def keyword_arguments_for( self, parameters: Dict[str, bool], - arguments: Optional[Dict[str, Any]] = None, + arguments: Dict[str, Any], ) -> Dict[str, Any]: """Generate the keyword arguments for a list of parameters.""" - if arguments is None: - arguments = {} + ret = {} for param, required in parameters.items(): if param in arguments: continue try: - arguments[param] = getattr(self, param) + ret[param] = getattr(self, param) except AttributeError: if required: raise @@ -260,7 +259,7 @@ class FileProcessor: "but this is not an available parameter.", param, ) - return arguments + return ret def generate_tokens(self) -> Generator[tokenize.TokenInfo, None, None]: """Tokenize the file and yield the tokens.""" diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index fb6664a..e8ebbc1 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -209,21 +209,21 @@ def test_next_line(default_options): [ ( {"blank_before": True, "blank_lines": True}, - None, + {}, {"blank_before": 0, "blank_lines": 0}, ), ( {"noqa": True, "fake": True}, {"fake": "foo"}, - {"noqa": False, "fake": "foo"}, + {"noqa": False}, ), ( {"blank_before": True, "blank_lines": True, "noqa": True}, {"blank_before": 10, "blank_lines": 5, "noqa": True}, - {"blank_before": 10, "blank_lines": 5, "noqa": True}, + {}, ), - ({}, {"fake": "foo"}, {"fake": "foo"}), - ({"non-existent": False}, {"fake": "foo"}, {"fake": "foo"}), + ({}, {"fake": "foo"}, {}), + ({"non-existent": False}, {"fake": "foo"}, {}), ], ) def test_keyword_arguments_for(params, args, expected_kwargs, default_options): @@ -235,9 +235,9 @@ def test_keyword_arguments_for(params, args, expected_kwargs, default_options): "Line 1", ], ) - kwargs_for = file_processor.keyword_arguments_for + ret = file_processor.keyword_arguments_for(params, args) - assert kwargs_for(params, args) == expected_kwargs + assert ret == expected_kwargs def test_keyword_arguments_for_does_not_handle_attribute_errors( @@ -253,7 +253,7 @@ def test_keyword_arguments_for_does_not_handle_attribute_errors( ) with pytest.raises(AttributeError): - file_processor.keyword_arguments_for({"fake": True}) + file_processor.keyword_arguments_for({"fake": True}, {}) def test_processor_split_line(default_options): From 72a02b9fdc5c4e30bfa14bf0f9a30ceb4a06cfb2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 19:27:18 -0500 Subject: [PATCH 078/257] remove function made unused by off_by_default refactor --- src/flake8/options/manager.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 896470d..b874210 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -407,24 +407,6 @@ class OptionManager: self.config_options_dict[name.replace("_", "-")] = option LOG.debug('Registered option "%s".', option) - def remove_from_default_ignore(self, error_codes: Sequence[str]) -> None: - """Remove specified error codes from the default ignore list. - - :param error_codes: - List of strings that are the error/warning codes to attempt to - remove from the extended default ignore list. - """ - LOG.debug("Removing %r from the default ignore list", error_codes) - for error_code in error_codes: - try: - self.extended_default_ignore.remove(error_code) - except (ValueError, KeyError): - LOG.debug( - "Attempted to remove %s from default ignore" - " but it was not a member of the list.", - error_code, - ) - def extend_default_ignore(self, error_codes: Sequence[str]) -> None: """Extend the default ignore list with the error codes provided. From f0f71fc1796a3f30f218b63587f3970ce023ee54 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 19:25:06 -0500 Subject: [PATCH 079/257] remove unused parameter from make_formatter --- src/flake8/api/legacy.py | 3 +-- src/flake8/main/application.py | 5 +---- tests/unit/test_legacy_api.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 8fb7ab6..5881aa9 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -158,8 +158,7 @@ class StyleGuide: "Report should be subclass of " "flake8.formatter.BaseFormatter." ) - self._application.formatter = None - self._application.make_formatter(reporter) + self._application.formatter = reporter(self.options) self._application.guide = None # NOTE(sigmavirus24): This isn't the intended use of # Application#make_guide but it works pretty well. diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index df32f92..111ed0c 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -10,7 +10,6 @@ from typing import Optional from typing import Sequence from typing import Set from typing import Tuple -from typing import Type import flake8 from flake8 import checker @@ -183,9 +182,7 @@ class Application: except TypeError: parse_options(self.options) - def make_formatter( - self, formatter_class: Optional[Type[BaseFormatter]] = None - ) -> None: + def make_formatter(self) -> None: """Initialize a formatter based on the parsed options.""" assert self.plugins is not None assert self.options is not None diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 169969b..0e5b535 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -122,7 +122,7 @@ def test_styleguide_init_report(): raise NotImplementedError style_guide.init_report(FakeFormatter) - app.make_formatter.assert_called_once_with(FakeFormatter) + assert isinstance(app.formatter, FakeFormatter) assert app.guide is None app.make_guide.assert_called_once_with() From 86bdc0dbc813aec27857fc06a703275896439c0d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 20:17:37 -0500 Subject: [PATCH 080/257] upgrade mccabe to 0.7.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2e0987c..f230cfe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ package_dir = # And in which releases we will update those ranges here: # http://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 install_requires = - mccabe>=0.6.0,<0.7.0 + mccabe>=0.7.0,<0.8.0 pycodestyle>=2.8.0,<2.9.0 pyflakes>=2.4.0,<2.5.0 importlib-metadata<4.3;python_version<"3.8" From d2333c4471754355641ca4fa78295dd0dcf6b2f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 20:40:34 -0500 Subject: [PATCH 081/257] include the file path in the plugin execution error --- src/flake8/checker.py | 4 +++- src/flake8/exceptions.py | 21 ++++++++++++++------- tests/unit/test_exceptions.py | 1 + tests/unit/test_file_checker.py | 7 ++++++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 2e8117f..28c954c 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -356,7 +356,9 @@ class FileChecker: exc_info=True, ) raise exceptions.PluginExecutionFailed( - plugin_name=plugin.display_name, exception=all_exc + filename=self.filename, + plugin_name=plugin.display_name, + exception=all_exc, ) @staticmethod diff --git a/src/flake8/exceptions.py b/src/flake8/exceptions.py index e2dfd77..8e13cd8 100644 --- a/src/flake8/exceptions.py +++ b/src/flake8/exceptions.py @@ -54,17 +54,24 @@ class PluginRequestedUnknownParameters(Flake8Exception): class PluginExecutionFailed(Flake8Exception): """The plugin failed during execution.""" - FORMAT = '"%(name)s" failed during execution due to "%(exc)s"' + FORMAT = '{fname}: "{plugin}" failed during execution due to {exc!r}' - def __init__(self, plugin_name: str, exception: Exception) -> None: + def __init__( + self, + filename: str, + plugin_name: str, + exception: Exception, + ) -> None: """Utilize keyword arguments for message generation.""" + self.filename = filename self.plugin_name = plugin_name self.original_exception = exception - super().__init__(plugin_name, exception) + super().__init__(filename, plugin_name, exception) def __str__(self) -> str: """Format our exception message.""" - return self.FORMAT % { - "name": self.plugin_name, - "exc": self.original_exception, - } + return self.FORMAT.format( + fname=self.filename, + plugin=self.plugin_name, + exc=self.original_exception, + ) diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 06c5179..96d0244 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -18,6 +18,7 @@ from flake8 import exceptions exception=ValueError("boom!"), ), exceptions.PluginExecutionFailed( + filename="filename.py", plugin_name="plugin_name", exception=ValueError("boom!"), ), diff --git a/tests/unit/test_file_checker.py b/tests/unit/test_file_checker.py index ee4f745..3fe2e51 100644 --- a/tests/unit/test_file_checker.py +++ b/tests/unit/test_file_checker.py @@ -54,5 +54,10 @@ def test_raises_exception_on_failed_plugin(tmp_path, default_options): plugins=finder.Checkers([], [], []), options=default_options, ) - with pytest.raises(flake8.exceptions.PluginExecutionFailed): + with pytest.raises(flake8.exceptions.PluginExecutionFailed) as excinfo: fchecker.run_check(plugin) + expected = ( + f'{fname}: "plugin-name[X]" failed during execution ' + f"due to ValueError()" + ) + assert str(excinfo.value) == expected From 0bb55d36f2e5b8b4d5240871d7fc7d6a373598ce Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 20:55:08 -0500 Subject: [PATCH 082/257] remove slow debug() log flake8 spends ~5-6% of `flake8 -j1 src` on this line --- src/flake8/checker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 28c954c..2bbd6f2 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -337,7 +337,6 @@ class FileChecker: def run_check(self, plugin: LoadedPlugin, **arguments: Any) -> Any: """Run the check in a single plugin.""" - LOG.debug("Running %r with %r", plugin, arguments) assert self.processor is not None try: params = self.processor.keyword_arguments_for( From 4e56fc0f6a8d1d9dde4de14abcba38f54ea6edbe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Jan 2022 13:30:36 -0500 Subject: [PATCH 083/257] pregenerate the pycodestyle plugin to avoid call overhead --- bin/gen-pycodestyle-plugin | 96 +++++++++++++++++++ setup.cfg | 38 +------- src/flake8/plugins/pycodestyle.py | 123 +++++++++++++++++++++++++ tests/integration/test_plugins.py | 2 +- tests/unit/plugins/pycodestyle_test.py | 33 +++++++ 5 files changed, 255 insertions(+), 37 deletions(-) create mode 100755 bin/gen-pycodestyle-plugin create mode 100644 src/flake8/plugins/pycodestyle.py create mode 100644 tests/unit/plugins/pycodestyle_test.py diff --git a/bin/gen-pycodestyle-plugin b/bin/gen-pycodestyle-plugin new file mode 100755 index 0000000..3540a9a --- /dev/null +++ b/bin/gen-pycodestyle-plugin @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +import inspect +import os.path +from typing import Any +from typing import Callable +from typing import Generator +from typing import NamedTuple +from typing import Tuple + +import pycodestyle + + +def _too_long(s: str) -> str: + if len(s) >= 80: + return f"{s} # noqa: E501" + else: + return s + + +class Call(NamedTuple): + name: str + is_generator: bool + params: Tuple[str, ...] + + def to_src(self) -> str: + params_s = ", ".join(self.params) + if self.is_generator: + return _too_long(f" yield from _{self.name}({params_s})") + else: + lines = ( + _too_long(f" ret = _{self.name}({params_s})"), + " if ret is not None:", + " yield ret", + ) + return "\n".join(lines) + + @classmethod + def from_func(cls, func: Callable[..., Any]) -> "Call": + spec = inspect.getfullargspec(func) + params = tuple(spec.args) + return cls(func.__name__, inspect.isgeneratorfunction(func), params) + + +def lines() -> Generator[str, None, None]: + logical = [] + physical = [] + + logical = [ + Call.from_func(check) for check in pycodestyle._checks["logical_line"] + ] + physical = [ + Call.from_func(check) for check in pycodestyle._checks["physical_line"] + ] + assert not pycodestyle._checks["tree"] + + yield f'"""Generated using ./bin/{os.path.basename(__file__)}."""' + yield "# fmt: off" + yield "from typing import Any" + yield "from typing import Generator" + yield "from typing import Tuple" + yield "" + imports = sorted(call.name for call in logical + physical) + for name in imports: + yield _too_long(f"from pycodestyle import {name} as _{name}") + yield "" + yield "" + + yield "def pycodestyle_logical(" + logical_params = {param for call in logical for param in call.params} + for param in sorted(logical_params): + yield f" {param}: Any," + yield ") -> Generator[Tuple[int, str], None, None]:" + yield ' """Run pycodestyle logical checks."""' + for call in sorted(logical): + yield call.to_src() + yield "" + yield "" + + yield "def pycodestyle_physical(" + physical_params = {param for call in physical for param in call.params} + for param in sorted(physical_params): + yield f" {param}: Any," + yield ") -> Generator[Tuple[int, str], None, None]:" + yield ' """Run pycodestyle physical checks."""' + for call in sorted(physical): + yield call.to_src() + + +def main() -> int: + for line in lines(): + print(line) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/setup.cfg b/setup.cfg index f230cfe..9e199c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,42 +53,8 @@ console_scripts = flake8 = flake8.main.cli:main flake8.extension = F = flake8.plugins.pyflakes:FlakesChecker - pycodestyle.ambiguous_identifier = pycodestyle:ambiguous_identifier - pycodestyle.bare_except = pycodestyle:bare_except - pycodestyle.blank_lines = pycodestyle:blank_lines - pycodestyle.break_after_binary_operator = pycodestyle:break_after_binary_operator - pycodestyle.break_before_binary_operator = pycodestyle:break_before_binary_operator - pycodestyle.comparison_negative = pycodestyle:comparison_negative - pycodestyle.comparison_to_singleton = pycodestyle:comparison_to_singleton - pycodestyle.comparison_type = pycodestyle:comparison_type - pycodestyle.compound_statements = pycodestyle:compound_statements - pycodestyle.continued_indentation = pycodestyle:continued_indentation - pycodestyle.explicit_line_join = pycodestyle:explicit_line_join - pycodestyle.extraneous_whitespace = pycodestyle:extraneous_whitespace - pycodestyle.imports_on_separate_lines = pycodestyle:imports_on_separate_lines - pycodestyle.indentation = pycodestyle:indentation - pycodestyle.maximum_doc_length = pycodestyle:maximum_doc_length - pycodestyle.maximum_line_length = pycodestyle:maximum_line_length - pycodestyle.missing_whitespace = pycodestyle:missing_whitespace - pycodestyle.missing_whitespace_after_import_keyword = pycodestyle:missing_whitespace_after_import_keyword - pycodestyle.missing_whitespace_around_operator = pycodestyle:missing_whitespace_around_operator - pycodestyle.module_imports_on_top_of_file = pycodestyle:module_imports_on_top_of_file - pycodestyle.python_3000_async_await_keywords = pycodestyle:python_3000_async_await_keywords - pycodestyle.python_3000_backticks = pycodestyle:python_3000_backticks - pycodestyle.python_3000_has_key = pycodestyle:python_3000_has_key - pycodestyle.python_3000_invalid_escape_sequence = pycodestyle:python_3000_invalid_escape_sequence - pycodestyle.python_3000_not_equal = pycodestyle:python_3000_not_equal - pycodestyle.python_3000_raise_comma = pycodestyle:python_3000_raise_comma - pycodestyle.tabs_obsolete = pycodestyle:tabs_obsolete - pycodestyle.tabs_or_spaces = pycodestyle:tabs_or_spaces - pycodestyle.trailing_blank_lines = pycodestyle:trailing_blank_lines - pycodestyle.trailing_whitespace = pycodestyle:trailing_whitespace - pycodestyle.whitespace_around_comma = pycodestyle:whitespace_around_comma - pycodestyle.whitespace_around_keywords = pycodestyle:whitespace_around_keywords - pycodestyle.whitespace_around_named_parameter_equals = pycodestyle:whitespace_around_named_parameter_equals - pycodestyle.whitespace_around_operator = pycodestyle:whitespace_around_operator - pycodestyle.whitespace_before_comment = pycodestyle:whitespace_before_comment - pycodestyle.whitespace_before_parameters = pycodestyle:whitespace_before_parameters + E = flake8.plugins.pycodestyle:pycodestyle_logical + W = flake8.plugins.pycodestyle:pycodestyle_physical flake8.report = default = flake8.formatting.default:Default pylint = flake8.formatting.default:Pylint diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py new file mode 100644 index 0000000..e8011b2 --- /dev/null +++ b/src/flake8/plugins/pycodestyle.py @@ -0,0 +1,123 @@ +"""Generated using ./bin/gen-pycodestyle-plugin.""" +# fmt: off +from typing import Any +from typing import Generator +from typing import Tuple + +from pycodestyle import ambiguous_identifier as _ambiguous_identifier +from pycodestyle import bare_except as _bare_except +from pycodestyle import blank_lines as _blank_lines +from pycodestyle import break_after_binary_operator as _break_after_binary_operator # noqa: E501 +from pycodestyle import break_before_binary_operator as _break_before_binary_operator # noqa: E501 +from pycodestyle import comparison_negative as _comparison_negative +from pycodestyle import comparison_to_singleton as _comparison_to_singleton +from pycodestyle import comparison_type as _comparison_type +from pycodestyle import compound_statements as _compound_statements +from pycodestyle import continued_indentation as _continued_indentation +from pycodestyle import explicit_line_join as _explicit_line_join +from pycodestyle import extraneous_whitespace as _extraneous_whitespace +from pycodestyle import imports_on_separate_lines as _imports_on_separate_lines +from pycodestyle import indentation as _indentation +from pycodestyle import maximum_doc_length as _maximum_doc_length +from pycodestyle import maximum_line_length as _maximum_line_length +from pycodestyle import missing_whitespace as _missing_whitespace +from pycodestyle import missing_whitespace_after_import_keyword as _missing_whitespace_after_import_keyword # noqa: E501 +from pycodestyle import missing_whitespace_around_operator as _missing_whitespace_around_operator # noqa: E501 +from pycodestyle import module_imports_on_top_of_file as _module_imports_on_top_of_file # noqa: E501 +from pycodestyle import python_3000_async_await_keywords as _python_3000_async_await_keywords # noqa: E501 +from pycodestyle import python_3000_backticks as _python_3000_backticks +from pycodestyle import python_3000_has_key as _python_3000_has_key +from pycodestyle import python_3000_invalid_escape_sequence as _python_3000_invalid_escape_sequence # noqa: E501 +from pycodestyle import python_3000_not_equal as _python_3000_not_equal +from pycodestyle import python_3000_raise_comma as _python_3000_raise_comma +from pycodestyle import tabs_obsolete as _tabs_obsolete +from pycodestyle import tabs_or_spaces as _tabs_or_spaces +from pycodestyle import trailing_blank_lines as _trailing_blank_lines +from pycodestyle import trailing_whitespace as _trailing_whitespace +from pycodestyle import whitespace_around_comma as _whitespace_around_comma +from pycodestyle import whitespace_around_keywords as _whitespace_around_keywords # noqa: E501 +from pycodestyle import whitespace_around_named_parameter_equals as _whitespace_around_named_parameter_equals # noqa: E501 +from pycodestyle import whitespace_around_operator as _whitespace_around_operator # noqa: E501 +from pycodestyle import whitespace_before_comment as _whitespace_before_comment +from pycodestyle import whitespace_before_parameters as _whitespace_before_parameters # noqa: E501 + + +def pycodestyle_logical( + blank_before: Any, + blank_lines: Any, + checker_state: Any, + hang_closing: Any, + indent_char: Any, + indent_level: Any, + indent_size: Any, + line_number: Any, + lines: Any, + logical_line: Any, + max_doc_length: Any, + noqa: Any, + previous_indent_level: Any, + previous_logical: Any, + previous_unindented_logical_line: Any, + tokens: Any, + verbose: Any, +) -> Generator[Tuple[int, str], None, None]: + """Run pycodestyle logical checks.""" + yield from _ambiguous_identifier(logical_line, tokens) + yield from _bare_except(logical_line, noqa) + yield from _blank_lines(logical_line, blank_lines, indent_level, line_number, blank_before, previous_logical, previous_unindented_logical_line, previous_indent_level, lines) # noqa: E501 + yield from _break_after_binary_operator(logical_line, tokens) + yield from _break_before_binary_operator(logical_line, tokens) + yield from _comparison_negative(logical_line) + yield from _comparison_to_singleton(logical_line, noqa) + yield from _comparison_type(logical_line, noqa) + yield from _compound_statements(logical_line) + yield from _continued_indentation(logical_line, tokens, indent_level, hang_closing, indent_char, indent_size, noqa, verbose) # noqa: E501 + yield from _explicit_line_join(logical_line, tokens) + yield from _extraneous_whitespace(logical_line) + yield from _imports_on_separate_lines(logical_line) + yield from _indentation(logical_line, previous_logical, indent_char, indent_level, previous_indent_level, indent_size) # noqa: E501 + yield from _maximum_doc_length(logical_line, max_doc_length, noqa, tokens) + yield from _missing_whitespace(logical_line) + yield from _missing_whitespace_after_import_keyword(logical_line) + yield from _missing_whitespace_around_operator(logical_line, tokens) + yield from _module_imports_on_top_of_file(logical_line, indent_level, checker_state, noqa) # noqa: E501 + yield from _python_3000_async_await_keywords(logical_line, tokens) + yield from _python_3000_backticks(logical_line) + yield from _python_3000_has_key(logical_line, noqa) + yield from _python_3000_invalid_escape_sequence(logical_line, tokens, noqa) + yield from _python_3000_not_equal(logical_line) + yield from _python_3000_raise_comma(logical_line) + yield from _whitespace_around_comma(logical_line) + yield from _whitespace_around_keywords(logical_line) + yield from _whitespace_around_named_parameter_equals(logical_line, tokens) + yield from _whitespace_around_operator(logical_line) + yield from _whitespace_before_comment(logical_line, tokens) + yield from _whitespace_before_parameters(logical_line, tokens) + + +def pycodestyle_physical( + indent_char: Any, + line_number: Any, + lines: Any, + max_line_length: Any, + multiline: Any, + noqa: Any, + physical_line: Any, + total_lines: Any, +) -> Generator[Tuple[int, str], None, None]: + """Run pycodestyle physical checks.""" + ret = _maximum_line_length(physical_line, max_line_length, multiline, line_number, noqa) # noqa: E501 + if ret is not None: + yield ret + ret = _tabs_obsolete(physical_line) + if ret is not None: + yield ret + ret = _tabs_or_spaces(physical_line, indent_char) + if ret is not None: + yield ret + ret = _trailing_blank_lines(physical_line, lines, line_number, total_lines) + if ret is not None: + yield ret + ret = _trailing_whitespace(physical_line) + if ret is not None: + yield ret diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 03f0caf..bcf4d61 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -104,7 +104,7 @@ def test_local_plugin_can_add_option(local_config): args = aggregator.aggregate_options(option_manager, cfg, cfg_dir, argv) - assert args.extended_default_select == {"XE", "F", "E", "C90"} + assert args.extended_default_select == {"XE", "F", "E", "W", "C90"} assert args.anopt == "foo" diff --git a/tests/unit/plugins/pycodestyle_test.py b/tests/unit/plugins/pycodestyle_test.py new file mode 100644 index 0000000..703970f --- /dev/null +++ b/tests/unit/plugins/pycodestyle_test.py @@ -0,0 +1,33 @@ +import importlib.machinery +import importlib.util +import os.path + +import flake8.plugins.pycodestyle + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def test_up_to_date(): + """Validate that the generated pycodestyle plugin is up to date. + + We generate two "meta" plugins for pycodestyle to avoid calling overhead. + + To regenerate run: + + ./bin/gen-pycodestyle-plugin > src/flake8/plugins/pycodestyle.py + """ + + path = os.path.join(HERE, "../../../bin/gen-pycodestyle-plugin") + name = os.path.basename(path) + loader = importlib.machinery.SourceFileLoader(name, path) + spec = importlib.util.spec_from_loader(loader.name, loader) + assert spec is not None + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + + expected = "".join(f"{line}\n" for line in mod.lines()) + + with open(flake8.plugins.pycodestyle.__file__) as f: + contents = f.read() + + assert contents == expected From 19e83b0d88412e5fa1e415fcecac5b7e5e93d192 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 23:32:49 +0000 Subject: [PATCH 084/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v2.6.0 → v2.7.1](https://github.com/asottile/reorder_python_imports/compare/v2.6.0...v2.7.1) - [github.com/psf/black: 21.12b0 → 22.1.0](https://github.com/psf/black/compare/21.12b0...22.1.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b6722e..3027345 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,12 +8,12 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v2.6.0 + rev: v2.7.1 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.1.0 hooks: - id: black args: [--line-length=79] From 58ade57ca2abebfb038c656dccf3b02fe86dc871 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Feb 2022 08:14:26 -0800 Subject: [PATCH 085/257] re-show pycodestyle in help after plugin gen --- src/flake8/plugins/finder.py | 3 +-- tests/unit/plugins/finder_test.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 43b5417..3731db9 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -162,12 +162,11 @@ def _flake8_plugins( if ep.name == "F": yield Plugin(pyflakes_meta["name"], pyflakes_meta["version"], ep) - elif ep.name.startswith("pycodestyle"): + elif ep.name in "EW": # pycodestyle provides both `E` and `W` -- but our default select # handles those # ideally pycodestyle's plugin entrypoints would exactly represent # the codes they produce... - ep = importlib_metadata.EntryPoint("E", ep.value, ep.group) yield Plugin( pycodestyle_meta["name"], pycodestyle_meta["version"], ep ) diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 15f15a2..e1ab858 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -118,8 +118,8 @@ flake8 = flake8.main.cli:main [flake8.extension] F = flake8.plugins.pyflakes:FlakesChecker -pycodestyle.bare_except = pycodestyle:bare_except -pycodestyle.blank_lines = pycodestyle:blank_lines +E = flake8.plugins.pycodestyle:pycodestyle_logical +W = flake8.plugins.pycodestyle:pycodestyle_physical [flake8.report] default = flake8.formatting.default:Default @@ -181,7 +181,7 @@ def test_flake8_plugins(flake8_dist, mock_distribution): "9000.2.0", importlib_metadata.EntryPoint( "E", - "pycodestyle:bare_except", + "flake8.plugins.pycodestyle:pycodestyle_logical", "flake8.extension", ), ), @@ -189,8 +189,8 @@ def test_flake8_plugins(flake8_dist, mock_distribution): "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "E", - "pycodestyle:blank_lines", + "W", + "flake8.plugins.pycodestyle:pycodestyle_physical", "flake8.extension", ), ), @@ -276,7 +276,7 @@ unrelated = unrelated:main "9000.2.0", importlib_metadata.EntryPoint( "E", - "pycodestyle:bare_except", + "flake8.plugins.pycodestyle:pycodestyle_logical", "flake8.extension", ), ), @@ -284,8 +284,8 @@ unrelated = unrelated:main "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "E", - "pycodestyle:blank_lines", + "W", + "flake8.plugins.pycodestyle:pycodestyle_physical", "flake8.extension", ), ), @@ -512,7 +512,7 @@ def test_find_plugins( "9000.2.0", importlib_metadata.EntryPoint( "E", - "pycodestyle:bare_except", + "flake8.plugins.pycodestyle:pycodestyle_logical", "flake8.extension", ), ), @@ -520,8 +520,8 @@ def test_find_plugins( "pycodestyle", "9000.2.0", importlib_metadata.EntryPoint( - "E", - "pycodestyle:blank_lines", + "W", + "flake8.plugins.pycodestyle:pycodestyle_physical", "flake8.extension", ), ), From f7ef1a6c8bd52106bf40747146307e2dc2e46cad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Feb 2022 08:36:36 -0800 Subject: [PATCH 086/257] make --ignore order consistent --- src/flake8/options/aggregator.py | 8 +++----- src/flake8/options/manager.py | 9 ++++----- tests/integration/test_plugins.py | 2 +- tests/unit/test_option_manager.py | 4 ++-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index 0311257..4b40883 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -35,11 +35,9 @@ def aggregate_options( default_values.extended_default_ignore = ( manager.extended_default_ignore.copy() ) - LOG.debug( - "Extended default ignore list: %s", list(extended_default_ignore) - ) - extended_default_ignore.update(default_values.ignore) - default_values.ignore = list(extended_default_ignore) + LOG.debug("Extended default ignore list: %s", extended_default_ignore) + extended_default_ignore.extend(default_values.ignore) + default_values.ignore = extended_default_ignore LOG.debug("Merged default ignore list: %s", default_values.ignore) extended_default_select = manager.extended_default_select.copy() diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index b874210..ff5a229 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -10,7 +10,6 @@ from typing import List from typing import Mapping from typing import Optional from typing import Sequence -from typing import Set from typing import Tuple from typing import Type from typing import Union @@ -353,8 +352,8 @@ class OptionManager: self.config_options_dict: Dict[str, Option] = {} self.options: List[Option] = [] - self.extended_default_ignore: Set[str] = set() - self.extended_default_select: Set[str] = set() + self.extended_default_ignore: List[str] = [] + self.extended_default_select: List[str] = [] self._current_group: Optional[argparse._ArgumentGroup] = None @@ -415,7 +414,7 @@ class OptionManager: extend the default ignore list. """ LOG.debug("Extending default ignore list with %r", error_codes) - self.extended_default_ignore.update(error_codes) + self.extended_default_ignore.extend(error_codes) def extend_default_select(self, error_codes: Sequence[str]) -> None: """Extend the default select list with the error codes provided. @@ -425,7 +424,7 @@ class OptionManager: to extend the default select list. """ LOG.debug("Extending default select list with %r", error_codes) - self.extended_default_select.update(error_codes) + self.extended_default_select.extend(error_codes) def parse_args( self, diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index bcf4d61..4323d05 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -104,7 +104,7 @@ def test_local_plugin_can_add_option(local_config): args = aggregator.aggregate_options(option_manager, cfg, cfg_dir, argv) - assert args.extended_default_select == {"XE", "F", "E", "W", "C90"} + assert args.extended_default_select == ["XE", "C90", "F", "E", "W"] assert args.anopt == "foo" diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index a5e3d33..0a56076 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -156,10 +156,10 @@ def test_parse_args_normalize_paths(optmanager): def test_extend_default_ignore(optmanager): """Verify that we update the extended default ignore list.""" - assert optmanager.extended_default_ignore == set() + assert optmanager.extended_default_ignore == [] optmanager.extend_default_ignore(["T100", "T101", "T102"]) - assert optmanager.extended_default_ignore == {"T100", "T101", "T102"} + assert optmanager.extended_default_ignore == ["T100", "T101", "T102"] def test_optparse_normalize_callback_option_legacy(optmanager): From 54cf6ea0b18c1768658c772297782bf6ab9c73eb Mon Sep 17 00:00:00 2001 From: Tomislav Maricevic Date: Thu, 10 Feb 2022 10:33:19 +0100 Subject: [PATCH 087/257] Clarify that --count writes to standard output --- src/flake8/main/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index cb4cd73..3479893 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -157,7 +157,7 @@ def register_default_options(option_manager: OptionManager) -> None: "--count", action="store_true", parse_from_config=True, - help="Print total number of errors and warnings to standard error and" + help="Print total number of errors and warnings to standard output and" " set the exit code to 1 if total is not empty.", ) From 430f55e6d7ce66c0c0b0cf8b938baa4a1fb552ff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Feb 2022 09:41:44 -0500 Subject: [PATCH 088/257] remove manpage the suggestion for packagers is to utilize help2man or similar tools to get an up-to-date manpage --- docs/source/conf.py | 6 -- docs/source/index.rst | 1 - docs/source/manpage.rst | 148 ---------------------------------------- tox.ini | 1 - 4 files changed, 156 deletions(-) delete mode 100644 docs/source/manpage.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 2c9ea0b..54d837c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -259,12 +259,6 @@ latex_documents = [ # -- Options for manual page output --------------------------------------- -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ("manpage", "flake8", "Flake8 Command Line Documentation", [author], 1) -] - # If true, show URL addresses after external links. # man_show_urls = False diff --git a/docs/source/index.rst b/docs/source/index.rst index 9488985..51b0189 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -89,7 +89,6 @@ and how to specify them on the command-line or in configuration files. :maxdepth: 2 user/index - Flake8 man page Plugin Developer Guide ====================== diff --git a/docs/source/manpage.rst b/docs/source/manpage.rst deleted file mode 100644 index aa3e7b4..0000000 --- a/docs/source/manpage.rst +++ /dev/null @@ -1,148 +0,0 @@ -======== - flake8 -======== - -SYNOPSIS -======== - -.. code:: - - flake8 [options] [ ...] - - flake8 --help - -DESCRIPTION -=========== - -``flake8`` is a command-line utility for enforcing style consistency across -Python projects. By default it includes lint checks provided by the PyFlakes -project, PEP-0008 inspired style checks provided by the PyCodeStyle project, -and McCabe complexity checking provided by the McCabe project. It will also -run third-party extensions if they are found and installed. - -OPTIONS -======= - -It is important to note that third-party extensions may add options which are -not represented here. To see all options available in your installation, run:: - - flake8 --help - -All options available as of Flake8 3.1.0:: - - --version show program's version number and exit - -h, --help show this help message and exit - -v, --verbose Print more information about what is happening in - flake8. This option is repeatable and will increase - verbosity each time it is repeated. - -q, --quiet Report only file names, or nothing. This option is - repeatable. - --count Print total number of errors and warnings to standard - error and set the exit code to 1 if total is not - empty. - --diff Report changes only within line number ranges in the - unified diff provided on standard in by the user. - --exclude=patterns Comma-separated list of files or directories to - exclude. (Default: - .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.nox,.eggs, - *.egg) - --filename=patterns Only check for filenames matching the patterns in this - comma-separated list. (Default: *.py) - --stdin-display-name=STDIN_DISPLAY_NAME - The name used when reporting errors from code passed - via stdin. This is useful for editors piping the file - contents to flake8. (Default: stdin) - --format=format Format errors according to the chosen formatter. - --hang-closing Hang closing bracket instead of matching indentation - of opening bracket's line. - --ignore=errors Comma-separated list of errors and warnings to ignore - (or skip). For example, ``--ignore=E4,E51,W234``. - (Default: E121,E123,E126,E226,E24,E704,W503,W504) - --max-line-length=n Maximum allowed line length for the entirety of this - run. (Default: 79) - --select=errors Comma-separated list of errors and warnings to enable. - For example, ``--select=E4,E51,W234``. (Default: - E,F,W,C90) - --disable-noqa Disable the effect of "# noqa". This will report - errors on lines with "# noqa" at the end. - --show-source Show the source generate each error or warning. - --statistics Count errors and warnings. - --enable-extensions=ENABLE_EXTENSIONS - Enable plugins and extensions that are otherwise - disabled by default - --exit-zero Exit with status code "0" even if there are errors. - -j JOBS, --jobs=JOBS Number of subprocesses to use to run checks in - parallel. This is ignored on Windows. The default, - "auto", will auto-detect the number of processors - available to use. (Default: auto) - --output-file=OUTPUT_FILE - Redirect report to a file. - --tee Write to stdout and output-file. - --append-config=APPEND_CONFIG - Provide extra config files to parse in addition to the - files found by Flake8 by default. These files are the - last ones read and so they take the highest precedence - when multiple files provide the same option. - --config=CONFIG Path to the config file that will be the authoritative - config source. This will cause Flake8 to ignore all - other configuration files. - --isolated Ignore all configuration files. - --benchmark Print benchmark information about this run of Flake8 - --bug-report Print information necessary when preparing a bug - report - --builtins=BUILTINS define more built-ins, comma separated - --doctests check syntax of the doctests - --include-in-doctest=INCLUDE_IN_DOCTEST - Run doctests only on these files - --exclude-from-doctest=EXCLUDE_FROM_DOCTEST - Skip these files when running doctests - --max-complexity=MAX_COMPLEXITY - McCabe complexity threshold - -EXAMPLES -======== - -Simply running flake8 against the current directory:: - - flake8 - flake8 . - -Running flake8 against a specific path:: - - flake8 path/to/file.py - -Ignoring violations from flake8:: - - flake8 --ignore E101 - flake8 --ignore E1,E202 - -Only report certain violations:: - - flake8 --select E101 - flake8 --select E2,E742 - -Analyzing only a diff:: - - git diff -U0 | flake8 --diff - - -Generate information for a bug report:: - - flake8 --bug-report - -SEE ALSO -======== - -Flake8 documentation: http://flake8.pycqa.org - -Flake8 Options and Examples: http://flake8.pycqa.org/en/latest/user/options.html - -PyCodeStyle documentation: http://pycodestyle.pycqa.org - -PyFlakes: https://github.com/pycqa/pyflakes - -McCabe: https://github.com/pycqa/mccabe - -BUGS -==== - -Please report all bugs to https://github.com/pycqa/flake8 diff --git a/tox.ini b/tox.ini index 6b864e2..c0763c1 100644 --- a/tox.ini +++ b/tox.ini @@ -85,7 +85,6 @@ deps = -rdocs/source/requirements.txt commands = sphinx-build -E -W -c docs/source/ -b html docs/source/ docs/build/html - sphinx-build -E -W -c docs/source/ -b man docs/source/ docs/build/man [testenv:serve-docs] skip_install = true From c6882772e1640ff4c59f6d84986377813cf9236a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Feb 2022 15:47:58 -0500 Subject: [PATCH 089/257] where possible http->https and fix links --- CONTRIBUTING.rst | 2 +- README.rst | 12 ++++++------ docs/source/internal/contributing.rst | 2 +- docs/source/release-notes/3.0.0.rst | 2 +- docs/source/user/options.rst | 2 +- setup.cfg | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 94d2536..054dfef 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,3 +1,3 @@ Please refer to `Contributing to Flake8 -`_ +`_ on our website. diff --git a/README.rst b/README.rst index ddac952..23102af 100644 --- a/README.rst +++ b/README.rst @@ -37,14 +37,14 @@ Quickstart ========== See our `quickstart documentation -`_ for how to install +`_ for how to install and get started with Flake8. Frequently Asked Questions ========================== -Flake8 maintains an `FAQ `_ in its +Flake8 maintains an `FAQ `_ in its documentation. @@ -61,7 +61,7 @@ to suggest, the mailing list would be the best place for it. Links ===== -* `Flake8 Documentation `_ +* `Flake8 Documentation `_ * `GitHub Project `_ @@ -72,14 +72,14 @@ Links `_ * `Code of Conduct - `_ + `_ * `Getting Started Contributing - `_ + `_ Maintenance =========== Flake8 was created by Tarek Ziadé and is currently maintained by `Ian Cordasco -`_ +`_ diff --git a/docs/source/internal/contributing.rst b/docs/source/internal/contributing.rst index 26cc029..bd33015 100644 --- a/docs/source/internal/contributing.rst +++ b/docs/source/internal/contributing.rst @@ -197,7 +197,7 @@ delivered. .. links .. _Python Code Quality Authority's Code of Conduct: - http://meta.pycqa.org/en/latest/code-of-conduct.html + https://meta.pycqa.org/code-of-conduct.html .. _tox: https://tox.readthedocs.io/ diff --git a/docs/source/release-notes/3.0.0.rst b/docs/source/release-notes/3.0.0.rst index 2cec53a..28c0b7f 100644 --- a/docs/source/release-notes/3.0.0.rst +++ b/docs/source/release-notes/3.0.0.rst @@ -1,7 +1,7 @@ 3.0.0 -- 2016-07-25 ------------------- -- Rewrite our documentation from scratch! (http://flake8.pycqa.org) +- Rewrite our documentation from scratch! (https://flake8.pycqa.org) - Drop explicit support for Pythons 2.6, 3.2, and 3.3. diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index 8bf7e33..b622bf6 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -569,7 +569,7 @@ Options and their Descriptions # https://some-super-long-domain-name.com/with/some/very/long/path url = ( - 'http://...' + 'https://...' ) This defaults to: ``79`` diff --git a/setup.cfg b/setup.cfg index 9e199c4..acb152a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,9 +35,9 @@ packages = find: package_dir = =src # We document the reasoning for using ranges here: -# http://flake8.pycqa.org/en/latest/faq.html#why-does-flake8-use-ranges-for-its-dependencies +# https://flake8.pycqa.org/en/latest/faq.html#why-does-flake8-use-ranges-for-its-dependencies # And in which releases we will update those ranges here: -# http://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 +# https://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.8.0,<2.9.0 From 21f22765b505e586dcb693100f6cb50f3f638884 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 00:44:20 +0000 Subject: [PATCH 090/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v2.7.1 → v3.0.1](https://github.com/asottile/reorder_python_imports/compare/v2.7.1...v3.0.1) - [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1) - [github.com/pre-commit/mirrors-mypy: v0.931 → v0.940](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v0.940) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3027345..a18949b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v2.7.1 + rev: v3.0.1 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] @@ -22,12 +22,12 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.940 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From befc26f78ea6370fb4d46f9aeaa17f76d657e427 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Mar 2022 17:04:30 -0400 Subject: [PATCH 091/257] fix memory leak caused by lru_cache of a method --- src/flake8/style_guide.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index f26e738..5258485 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -240,6 +240,10 @@ class StyleGuideManager: ) ) + self.style_guide_for = functools.lru_cache(maxsize=None)( + self._style_guide_for + ) + def populate_style_guides_with( self, options: argparse.Namespace ) -> Generator["StyleGuide", None, None]: @@ -256,8 +260,7 @@ class StyleGuideManager: filename=filename, extend_ignore_with=violations ) - @functools.lru_cache(maxsize=None) - def style_guide_for(self, filename: str) -> "StyleGuide": + def _style_guide_for(self, filename: str) -> "StyleGuide": """Find the StyleGuide for the filename in particular.""" guides = sorted( (g for g in self.style_guides if g.applies_to(filename)), From f88b8a410a112601520c00c6ccbade21306ba655 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Mar 2022 17:00:26 -0400 Subject: [PATCH 092/257] hopefully reduce duplicates of pyproject.toml --- .github/ISSUE_TEMPLATE/02_feature.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml index 61fcf25..ee3f4b3 100644 --- a/.github/ISSUE_TEMPLATE/02_feature.yml +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -12,6 +12,10 @@ body: attributes: value: '**NOTE: flake8 is a linting framework and does not implement any checks**' + - type: markdown + attributes: + value: '**NOTE: if you ask about `pyproject.toml` your issue will be closed as a duplicate of #234**' + - type: textarea id: freeform attributes: From 2680339377d9fdb0e27fc1f2f30828a93c60d08a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Mar 2022 17:10:37 -0400 Subject: [PATCH 093/257] make this a link --- .github/ISSUE_TEMPLATE/02_feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml index ee3f4b3..1447f06 100644 --- a/.github/ISSUE_TEMPLATE/02_feature.yml +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -14,7 +14,7 @@ body: - type: markdown attributes: - value: '**NOTE: if you ask about `pyproject.toml` your issue will be closed as a duplicate of #234**' + value: '**NOTE: if you ask about `pyproject.toml` your issue will be closed as a duplicate of [#234](https://github.com/PyCQA/flake8/issues/234)**' - type: textarea id: freeform From bb2d30614eee61052595d786184d625add7b5445 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Mar 2022 00:56:17 +0000 Subject: [PATCH 094/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.940 → v0.941](https://github.com/pre-commit/mirrors-mypy/compare/v0.940...v0.941) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a18949b..4e93bce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.940 + rev: v0.941 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From c5225db626bd4eda1d3852d54ecba5adaf927508 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Mar 2022 18:10:32 -0700 Subject: [PATCH 095/257] simplify decision engine - not specified codes (cmdline / config) are now known as being implicit via None sentinel - removed redundant logic for (explicit, explicit) selection --- src/flake8/main/options.py | 56 +++--- src/flake8/options/aggregator.py | 20 +-- src/flake8/style_guide.py | 182 ++++++++------------ tests/integration/test_aggregator.py | 12 +- tests/unit/test_decision_engine.py | 247 +++++++-------------------- 5 files changed, 166 insertions(+), 351 deletions(-) diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 3479893..4c9dfb8 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -24,8 +24,8 @@ def stage1_arg_parser() -> argparse.ArgumentParser: "--verbose", default=0, action="count", - help="Print more information about what is happening in flake8." - " This option is repeatable and will increase verbosity each " + help="Print more information about what is happening in flake8. " + "This option is repeatable and will increase verbosity each " "time it is repeated.", ) @@ -157,8 +157,8 @@ def register_default_options(option_manager: OptionManager) -> None: "--count", action="store_true", parse_from_config=True, - help="Print total number of errors and warnings to standard output and" - " set the exit code to 1 if total is not empty.", + help="Print total number of errors to standard output and " + "set the exit code to 1 if total is not empty.", ) add_option( @@ -175,8 +175,8 @@ def register_default_options(option_manager: OptionManager) -> None: comma_separated_list=True, parse_from_config=True, normalize_paths=True, - help="Comma-separated list of files or directories to exclude." - " (Default: %(default)s)", + help="Comma-separated list of files or directories to exclude. " + "(Default: %(default)s)", ) add_option( @@ -186,8 +186,8 @@ def register_default_options(option_manager: OptionManager) -> None: parse_from_config=True, comma_separated_list=True, normalize_paths=True, - help="Comma-separated list of files or directories to add to the list" - " of excluded ones.", + help="Comma-separated list of files or directories to add to the list " + "of excluded ones.", ) add_option( @@ -203,9 +203,9 @@ def register_default_options(option_manager: OptionManager) -> None: add_option( "--stdin-display-name", default="stdin", - help="The name used when reporting errors from code passed via stdin." - " This is useful for editors piping the file contents to flake8." - " (Default: %(default)s)", + help="The name used when reporting errors from code passed via stdin. " + "This is useful for editors piping the file contents to flake8. " + "(Default: %(default)s)", ) # TODO(sigmavirus24): Figure out --first/--repeat @@ -225,28 +225,29 @@ def register_default_options(option_manager: OptionManager) -> None: "--hang-closing", action="store_true", parse_from_config=True, - help="Hang closing bracket instead of matching indentation of opening" - " bracket's line.", + help="Hang closing bracket instead of matching indentation of opening " + "bracket's line.", ) add_option( "--ignore", metavar="errors", - default=",".join(defaults.IGNORE), parse_from_config=True, comma_separated_list=True, - help="Comma-separated list of errors and warnings to ignore (or skip)." - " For example, ``--ignore=E4,E51,W234``. (Default: %(default)s)", + help=( + f"Comma-separated list of error codes to ignore (or skip). " + f"For example, ``--ignore=E4,E51,W234``. " + f"(Default: {','.join(defaults.IGNORE)})" + ), ) add_option( "--extend-ignore", metavar="errors", - default="", parse_from_config=True, comma_separated_list=True, - help="Comma-separated list of errors and warnings to add to the list" - " of ignored ones. For example, ``--extend-ignore=E4,E51,W234``.", + help="Comma-separated list of error codes to add to the list of " + "ignored ones. For example, ``--extend-ignore=E4,E51,W234``.", ) add_option( @@ -291,21 +292,22 @@ def register_default_options(option_manager: OptionManager) -> None: add_option( "--select", metavar="errors", - default=",".join(defaults.SELECT), parse_from_config=True, comma_separated_list=True, - help="Comma-separated list of errors and warnings to enable." - " For example, ``--select=E4,E51,W234``. (Default: %(default)s)", + help=( + f"Comma-separated list of error codes to enable. " + f"For example, ``--select=E4,E51,W234``. " + f"(Default: {','.join(defaults.SELECT)})" + ), ) add_option( "--extend-select", metavar="errors", - default="", parse_from_config=True, comma_separated_list=True, help=( - "Comma-separated list of errors and warnings to add to the list " + "Comma-separated list of error codes to add to the list " "of selected ones. For example, ``--extend-select=E4,E51,W234``." ), ) @@ -339,7 +341,7 @@ def register_default_options(option_manager: OptionManager) -> None: "--statistics", action="store_true", parse_from_config=True, - help="Count errors and warnings.", + help="Count errors.", ) # Flake8 options @@ -358,8 +360,8 @@ def register_default_options(option_manager: OptionManager) -> None: type=JobsArgument, help="Number of subprocesses to use to run checks in parallel. " 'This is ignored on Windows. The default, "auto", will ' - "auto-detect the number of processors available to use." - " (Default: %(default)s)", + "auto-detect the number of processors available to use. " + "(Default: %(default)s)", ) add_option( diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index 4b40883..580def6 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -28,23 +28,9 @@ def aggregate_options( # Get the parsed config parsed_config = config.parse_config(manager, cfg, cfg_dir) - # Extend the default ignore value with the extended default ignore list, - # registered by plugins. - extended_default_ignore = manager.extended_default_ignore.copy() - # Let's store our extended default ignore for use by the decision engine - default_values.extended_default_ignore = ( - manager.extended_default_ignore.copy() - ) - LOG.debug("Extended default ignore list: %s", extended_default_ignore) - extended_default_ignore.extend(default_values.ignore) - default_values.ignore = extended_default_ignore - LOG.debug("Merged default ignore list: %s", default_values.ignore) - - extended_default_select = manager.extended_default_select.copy() - LOG.debug( - "Extended default select list: %s", list(extended_default_select) - ) - default_values.extended_default_select = extended_default_select + # store the plugin-set extended default ignore / select + default_values.extended_default_ignore = manager.extended_default_ignore + default_values.extended_default_select = manager.extended_default_select # Merge values parsed from config onto the default values returned for config_name, value in parsed_config.items(): diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 5258485..d9f6dbc 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -47,6 +47,21 @@ class Decision(enum.Enum): Selected = "selected error" +def _select_ignore( + *, + option: Optional[List[str]], + default: Tuple[str, ...], + extended_default: List[str], + extend: Optional[List[str]], +) -> Tuple[str, ...]: + # option was explicitly set, ignore the default and extended default + if option is not None: + ret = [*option, *(extend or [])] + else: + ret = [*default, *extended_default, *(extend or [])] + return tuple(sorted(ret, reverse=True)) + + class DecisionEngine: """A class for managing the decision process around violations. @@ -57,26 +72,27 @@ class DecisionEngine: def __init__(self, options: argparse.Namespace) -> None: """Initialize the engine.""" self.cache: Dict[str, Decision] = {} - self.selected = tuple(options.select) - self.extended_selected = tuple( - sorted(options.extended_default_select, reverse=True) + + self.using_default_select = ( + options.select is None and options.extend_select is None ) - self.all_selected = tuple( - sorted( - itertools.chain(self.selected, options.extend_select), - reverse=True, - ) + self.using_default_ignore = ( + options.ignore is None and options.extend_ignore is None ) - self.ignored = tuple( - sorted( - itertools.chain(options.ignore, options.extend_ignore), - reverse=True, - ) + + self.selected = _select_ignore( + option=options.select, + default=defaults.SELECT, + extended_default=options.extended_default_select, + extend=options.extend_select, + ) + + self.ignored = _select_ignore( + option=options.ignore, + default=defaults.IGNORE, + extended_default=options.extended_default_ignore, + extend=options.extend_ignore, ) - self.using_default_ignore = set(self.ignored) == set( - defaults.IGNORE - ).union(options.extended_default_ignore) - self.using_default_select = set(self.selected) == set(defaults.SELECT) def was_selected(self, code: str) -> Union[Selected, Ignored]: """Determine if the code has been selected by the user. @@ -89,16 +105,13 @@ class DecisionEngine: Ignored.Implicitly if the selected list is not empty but no match was found. """ - if code.startswith(self.all_selected): - return Selected.Explicitly - - if not self.all_selected and code.startswith(self.extended_selected): - # If it was not explicitly selected, it may have been implicitly - # selected because the check comes from a plugin that is enabled by - # default - return Selected.Implicitly - - return Ignored.Implicitly + if code.startswith(self.selected): + if self.using_default_select: + return Selected.Implicitly + else: + return Selected.Explicitly + else: + return Ignored.Implicitly def was_ignored(self, code: str) -> Union[Selected, Ignored]: """Determine if the code has been ignored by the user. @@ -113,83 +126,54 @@ class DecisionEngine: was found. """ if code.startswith(self.ignored): - return Ignored.Explicitly - - return Selected.Implicitly - - def more_specific_decision_for(self, code: str) -> Decision: - select = find_first_match(code, self.all_selected) - extra_select = find_first_match(code, self.extended_selected) - ignore = find_first_match(code, self.ignored) - - if select and ignore: - # If the violation code appears in both the select and ignore - # lists (in some fashion) then if we're using the default ignore - # list and a custom select list we should select the code. An - # example usage looks like this: - # A user has a code that would generate an E126 violation which - # is in our default ignore list and they specify select=E. - # We should be reporting that violation. This logic changes, - # however, if they specify select and ignore such that both match. - # In that case we fall through to our find_more_specific call. - # If, however, the user hasn't specified a custom select, and - # we're using the defaults for both select and ignore then the - # more specific rule must win. In most cases, that will be to - # ignore the violation since our default select list is very - # high-level and our ignore list is highly specific. - if self.using_default_ignore and not self.using_default_select: - return Decision.Selected - return find_more_specific(select, ignore) - if extra_select and ignore: - # At this point, select is false-y. Now we need to check if the - # code is in our extended select list and our ignore list. This is - # a *rare* case as we see little usage of the extended select list - # that plugins can use, so I suspect this section may change to - # look a little like the block above in which we check if we're - # using our default ignore list. - return find_more_specific(extra_select, ignore) - if select or (extra_select and self.using_default_select): - # Here, ignore was false-y and the user has either selected - # explicitly the violation or the violation is covered by - # something in the extended select list and we're using the - # default select list. In either case, we want the violation to be - # selected. - return Decision.Selected - if select is None and ( - extra_select is None or not self.using_default_ignore - ): - return Decision.Ignored - if (select is None and not self.using_default_select) and ( - ignore is None and self.using_default_ignore - ): - return Decision.Ignored - return Decision.Selected + if self.using_default_ignore: + return Ignored.Implicitly + else: + return Ignored.Explicitly + else: + return Selected.Implicitly def make_decision(self, code: str) -> Decision: """Decide if code should be ignored or selected.""" - LOG.debug('Deciding if "%s" should be reported', code) selected = self.was_selected(code) ignored = self.was_ignored(code) LOG.debug( - 'The user configured "%s" to be "%s", "%s"', + "The user configured %r to be %r, %r", code, selected, ignored, ) - if ( - selected is Selected.Explicitly or selected is Selected.Implicitly - ) and ignored is Selected.Implicitly: - decision = Decision.Selected + if isinstance(selected, Selected) and isinstance(ignored, Selected): + return Decision.Selected + elif isinstance(selected, Ignored) and isinstance(ignored, Ignored): + return Decision.Ignored + elif ( + selected is Selected.Explicitly + and ignored is not Ignored.Explicitly + ): + return Decision.Selected + elif ( + selected is not Selected.Explicitly + and ignored is Ignored.Explicitly + ): + return Decision.Ignored + elif selected is Ignored.Implicitly and ignored is Selected.Implicitly: + return Decision.Ignored elif ( selected is Selected.Explicitly and ignored is Ignored.Explicitly ) or ( - selected is Ignored.Implicitly and ignored is Selected.Implicitly + selected is Selected.Implicitly and ignored is Ignored.Implicitly ): - decision = self.more_specific_decision_for(code) - elif selected is Ignored.Implicitly or ignored is Ignored.Explicitly: - decision = Decision.Ignored # pylint: disable=R0204 - return decision + # we only get here if it was in both lists: longest prefix wins + select = next(s for s in self.selected if code.startswith(s)) + ignore = next(s for s in self.ignored if code.startswith(s)) + if len(select) > len(ignore): + return Decision.Selected + else: + return Decision.Ignored + else: + raise AssertionError(f"unreachable {code} {selected} {ignored}") def decision_for(self, code: str) -> Decision: """Return the decision for a specific code. @@ -362,7 +346,8 @@ class StyleGuide: """Create a copy of this style guide with different values.""" filename = filename or self.filename options = copy.deepcopy(self.options) - options.ignore.extend(extend_ignore_with or []) + options.extend_ignore = options.extend_ignore or [] + options.extend_ignore.extend(extend_ignore_with or []) return StyleGuide( options, self.formatter, self.stats, filename=filename ) @@ -471,20 +456,3 @@ class StyleGuide: Dictionary mapping filenames to sets of line number ranges. """ self._parsed_diff = diffinfo - - -def find_more_specific(selected: str, ignored: str) -> Decision: - if selected.startswith(ignored) and selected != ignored: - return Decision.Selected - return Decision.Ignored - - -def find_first_match( - error_code: str, code_list: Tuple[str, ...] -) -> Optional[str]: - startswith = error_code.startswith - for code in code_list: - if startswith(code): - return code - else: - return None diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py index de9e6fb..d35266f 100644 --- a/tests/integration/test_aggregator.py +++ b/tests/integration/test_aggregator.py @@ -76,15 +76,5 @@ def test_aggregate_options_when_isolated(optmanager, flake8_config): options = aggregator.aggregate_options(optmanager, cfg, cfg_dir, arguments) assert options.select == ["E11", "E34", "E402", "W", "F"] - assert sorted(options.ignore) == [ - "E121", - "E123", - "E126", - "E226", - "E24", - "E704", - "E8", - "W503", - "W504", - ] + assert options.ignore is None assert options.exclude == [os.path.abspath("tests/*")] diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index 19d574a..1280385 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -3,18 +3,17 @@ import argparse import pytest -from flake8 import defaults from flake8 import style_guide def create_options(**kwargs): """Create and return an instance of argparse.Namespace.""" - kwargs.setdefault("select", []) - kwargs.setdefault("extended_default_ignore", []) + kwargs.setdefault("select", None) + kwargs.setdefault("ignore", None) + kwargs.setdefault("extend_select", None) + kwargs.setdefault("extend_ignore", None) kwargs.setdefault("extended_default_select", []) - kwargs.setdefault("extend_select", []) - kwargs.setdefault("ignore", []) - kwargs.setdefault("extend_ignore", []) + kwargs.setdefault("extended_default_ignore", []) kwargs.setdefault("disable_noqa", False) return argparse.Namespace(**kwargs) @@ -93,7 +92,7 @@ def test_was_selected_implicitly_selects_errors(): error_code = "E121" decider = style_guide.DecisionEngine( create_options( - select=[], + select=None, extended_default_select=["E"], ), ) @@ -121,13 +120,13 @@ def test_was_selected_excludes_errors(select_list, error_code): @pytest.mark.parametrize( "select_list,ignore_list,extend_ignore,error_code,expected", [ - (["E111", "E121"], [], [], "E111", style_guide.Decision.Selected), - (["E111", "E121"], [], [], "E112", style_guide.Decision.Ignored), - (["E111", "E121"], [], [], "E121", style_guide.Decision.Selected), - (["E111", "E121"], [], [], "E122", style_guide.Decision.Ignored), - (["E11", "E12"], [], [], "E132", style_guide.Decision.Ignored), - (["E2", "E12"], [], [], "E321", style_guide.Decision.Ignored), - (["E2", "E12"], [], [], "E410", style_guide.Decision.Ignored), + (["E111", "E121"], [], None, "E111", style_guide.Decision.Selected), + (["E111", "E121"], [], None, "E112", style_guide.Decision.Ignored), + (["E111", "E121"], [], None, "E121", style_guide.Decision.Selected), + (["E111", "E121"], [], None, "E122", style_guide.Decision.Ignored), + (["E11", "E12"], [], None, "E132", style_guide.Decision.Ignored), + (["E2", "E12"], [], None, "E321", style_guide.Decision.Ignored), + (["E2", "E12"], [], None, "E410", style_guide.Decision.Ignored), (["E11", "E121"], ["E1"], [], "E112", style_guide.Decision.Selected), (["E11", "E121"], [], ["E1"], "E112", style_guide.Decision.Selected), ( @@ -137,41 +136,41 @@ def test_was_selected_excludes_errors(select_list, error_code): "E122", style_guide.Decision.Ignored, ), - (["E11", "E12"], ["E13"], [], "E132", style_guide.Decision.Ignored), - (["E1", "E3"], ["E32"], [], "E321", style_guide.Decision.Ignored), - ([], ["E2", "E12"], [], "E410", style_guide.Decision.Ignored), + (["E11", "E12"], ["E13"], None, "E132", style_guide.Decision.Ignored), + (["E1", "E3"], ["E32"], None, "E321", style_guide.Decision.Ignored), + ([], ["E2", "E12"], None, "E410", style_guide.Decision.Ignored), ( ["E4"], ["E2", "E12", "E41"], - [], + None, "E410", style_guide.Decision.Ignored, ), ( ["E41"], ["E2", "E12", "E4"], - [], + None, "E410", style_guide.Decision.Selected, ), - (["E"], ["F"], [], "E410", style_guide.Decision.Selected), - (["F"], [], [], "E410", style_guide.Decision.Ignored), - (["E"], defaults.IGNORE, [], "E126", style_guide.Decision.Selected), - (["W"], defaults.IGNORE, [], "E126", style_guide.Decision.Ignored), - (["E"], defaults.IGNORE, [], "W391", style_guide.Decision.Ignored), - (["E", "W"], ["E13"], [], "E131", style_guide.Decision.Ignored), - (defaults.SELECT, ["E13"], [], "E131", style_guide.Decision.Ignored), + (["E"], ["F"], None, "E410", style_guide.Decision.Selected), + (["F"], [], None, "E410", style_guide.Decision.Ignored), + (["E"], None, None, "E126", style_guide.Decision.Selected), + (["W"], None, None, "E126", style_guide.Decision.Ignored), + (["E"], None, None, "W391", style_guide.Decision.Ignored), + (["E", "W"], ["E13"], None, "E131", style_guide.Decision.Ignored), + (None, ["E13"], None, "E131", style_guide.Decision.Ignored), ( - defaults.SELECT, - defaults.IGNORE, + None, + None, ["W391"], "E126", style_guide.Decision.Ignored, ), ( - defaults.SELECT, - defaults.IGNORE, - [], + None, + None, + None, "W391", style_guide.Decision.Selected, ), @@ -192,166 +191,36 @@ def test_decision_for( assert decider.decision_for(error_code) is expected -@pytest.mark.parametrize( - ( - "select", - "ignore", - "extended_default_ignore", - "extended_default_select", - "error_code", - "expected", - ), - [ - ( - defaults.SELECT, - [], - [], - ["I1"], - "I100", - style_guide.Decision.Selected, - ), - ( - defaults.SELECT, - [], - [], - ["I1"], - "I201", - style_guide.Decision.Ignored, - ), - ( - defaults.SELECT, - ["I2"], - [], - ["I1"], - "I101", - style_guide.Decision.Selected, - ), - ( - defaults.SELECT, - ["I2"], - [], - ["I1"], - "I201", - style_guide.Decision.Ignored, - ), - ( - defaults.SELECT, - ["I1"], - [], - ["I10"], - "I101", - style_guide.Decision.Selected, - ), - ( - defaults.SELECT, - ["I10"], - [], - ["I1"], - "I101", - style_guide.Decision.Ignored, - ), - ( - defaults.SELECT, - ["U401"], - [], - [], - "U401", - style_guide.Decision.Ignored, - ), - ( - ["E", "W"], - ["E13"], - [], - [], - "E131", - style_guide.Decision.Ignored, - ), - ( - ["E", "W"], - ["E13"], - [], - [], - "E126", - style_guide.Decision.Selected, - ), - (["E2"], ["E21"], [], [], "E221", style_guide.Decision.Selected), - (["E2"], ["E21"], [], [], "E212", style_guide.Decision.Ignored), - ( - ["F", "W"], - ["C90"], - [], - ["I1"], - "C901", - style_guide.Decision.Ignored, - ), - ( - ["E", "W"], - ["C"], - [], - [], - "E131", - style_guide.Decision.Selected, - ), - ( - defaults.SELECT, - defaults.IGNORE, - [], - ["G"], - "G101", - style_guide.Decision.Selected, - ), - ( - defaults.SELECT, - ["G1"], - [], - ["G"], - "G101", - style_guide.Decision.Ignored, - ), - ( - ["E", "W"], - defaults.IGNORE, - [], - ["I"], - "I101", - style_guide.Decision.Ignored, - ), - ( - ["E", "W", "I101"], - defaults.IGNORE + ("I101",), - ["I101"], - [], - "I101", - style_guide.Decision.Selected, - ), - ( - ["E", "W"], - defaults.IGNORE + ("I101",), - ["I101"], - [], - "I101", - style_guide.Decision.Ignored, - ), - # TODO(sigmavirus24) Figure out how to exercise the final catch-all - # return statement - ], -) -def test_more_specific_decision_for_logic( - select, - ignore, - extended_default_ignore, - extended_default_select, - error_code, - expected, -): - """Verify the logic of DecisionEngine.more_specific_decision_for.""" +def test_implicitly_selected_and_implicitly_ignored_defers_to_length(): decider = style_guide.DecisionEngine( create_options( - select=select, - ignore=ignore, - extended_default_select=extended_default_select, - extended_default_ignore=extended_default_ignore, + # no options selected by user + select=None, + ignore=None, + extend_select=None, + extend_ignore=None, + # a plugin is installed and extends default ignore + extended_default_select=["P"], + extended_default_ignore=["P002"], ), ) - assert decider.more_specific_decision_for(error_code) is expected + assert decider.decision_for("P001") is style_guide.Decision.Selected + assert decider.decision_for("P002") is style_guide.Decision.Ignored + + +def test_user_can_extend_select_to_enable_plugin_default_ignored(): + decider = style_guide.DecisionEngine( + create_options( + # user options --extend-select=P002 + select=None, + ignore=None, + extend_select=["P002"], + extend_ignore=None, + # a plugin is installed and extends default ignore + extended_default_select=["P"], + extended_default_ignore=["P002"], + ), + ) + + assert decider.decision_for("P002") is style_guide.Decision.Selected From 17080ed718735228bf30441639efcbe7e968325a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:47:55 +0000 Subject: [PATCH 096/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.1.0 → 22.3.0](https://github.com/psf/black/compare/22.1.0...22.3.0) - [github.com/pre-commit/mirrors-mypy: v0.941 → v0.942](https://github.com/pre-commit/mirrors-mypy/compare/v0.941...v0.942) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e93bce..f506065 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black args: [--line-length=79] @@ -27,7 +27,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.941 + rev: v0.942 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 77851c82f1cd817e54d61cf5fb183d4bbc638e7e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Apr 2022 15:59:30 -0400 Subject: [PATCH 097/257] reorder configuration to put formatters first --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f506065..e44a72d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,11 @@ repos: hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] +- repo: https://github.com/asottile/pyupgrade + rev: v2.31.1 + hooks: + - id: pyupgrade + args: [--py36-plus] - repo: https://github.com/psf/black rev: 22.3.0 hooks: @@ -21,11 +26,6 @@ repos: rev: 4.0.1 hooks: - id: flake8 -- repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 - hooks: - - id: pyupgrade - args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.942 hooks: From f3443f4a7836aa947de29466902127d527f0ab89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Apr 2022 16:29:25 -0400 Subject: [PATCH 098/257] forbid invalid plugin prefixes in plugin loading --- src/flake8/plugins/finder.py | 11 ++++++ tests/unit/plugins/finder_test.py | 60 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 3731db9..3c576e9 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -1,7 +1,9 @@ """Functions related to finding and loading plugins.""" import configparser import inspect +import itertools import logging +import re import sys from typing import Any from typing import Dict @@ -20,6 +22,8 @@ from flake8.exceptions import FailedToLoadPlugin LOG = logging.getLogger(__name__) +VALID_CODE = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII) + FLAKE8_GROUPS = frozenset(("flake8.extension", "flake8.report")) BANNED_PLUGINS = { @@ -328,6 +332,13 @@ def _classify_plugins( else: raise NotImplementedError(f"what plugin type? {loaded}") + for loaded in itertools.chain(tree, logical_line, physical_line): + if not VALID_CODE.match(loaded.entry_name): + raise ExecutionError( + f"plugin code for `{loaded.display_name}` does not match " + f"{VALID_CODE.pattern}" + ) + return Plugins( checkers=Checkers( tree=tree, diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index e1ab858..91cb11f 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -29,6 +29,37 @@ def _loaded(plugin=None, obj=None, parameters=None): return finder.LoadedPlugin(plugin, obj, parameters) +@pytest.mark.parametrize( + "s", + ( + "E", + "E1", + "E123", + "ABC", + "ABC1", + "ABC123", + ), +) +def test_valid_plugin_prefixes(s): + assert finder.VALID_CODE.match(s) + + +@pytest.mark.parametrize( + "s", + ( + "", + "A1234", + "ABCD", + "abc", + "a-b", + "☃", + "A𝟗", + ), +) +def test_invalid_plugin_prefixes(s): + assert finder.VALID_CODE.match(s) is None + + def test_loaded_plugin_entry_name_vs_display_name(): loaded = _loaded(_plugin(package="package-name", ep=_ep(name="Q"))) assert loaded.entry_name == "Q" @@ -761,6 +792,35 @@ def test_classify_plugins_enable_a_disabled_plugin(): ) +def test_classify_plugins_does_not_error_on_reporter_prefix(): + # these are ok, don't check their name + plugin = _plugin(ep=_ep(name="report-er", group="flake8.report")) + loaded = _loaded(plugin=plugin) + + opts = finder.PluginOptions.blank() + classified = finder._classify_plugins([loaded], opts) + + assert classified == finder.Plugins( + checkers=finder.Checkers([], [], []), + reporters={"report-er": loaded}, + disabled=[], + ) + + +def test_classify_plugins_errors_on_incorrect_checker_name(): + plugin = _plugin(ep=_ep(name="INVALID", group="flake8.extension")) + loaded = _loaded(plugin=plugin, parameters={"tree": True}) + + with pytest.raises(ExecutionError) as excinfo: + finder._classify_plugins([loaded], finder.PluginOptions.blank()) + + (msg,) = excinfo.value.args + assert msg == ( + "plugin code for `local[INVALID]` " + "does not match ^[A-Z]{1,3}[0-9]{0,3}$" + ) + + @pytest.mark.usefixtures("reset_sys") def test_load_plugins(): plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) From 4f63b4f3144e3c24f1014efe673d7646e51ad14d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 22:57:51 +0000 Subject: [PATCH 099/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0) - [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e44a72d..5955077 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: check-yaml - id: debug-statements @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py36-plus] From a626e659c45121fe5f2b449ddd0d810d77ec5096 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Apr 2022 13:38:35 -0700 Subject: [PATCH 100/257] remove outdated output-file example from docs --- docs/source/user/options.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index b622bf6..5eb8e35 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -911,7 +911,6 @@ Options and their Descriptions .. code-block:: ini - output-file = output.txt tee = True From 9f88dc3a6765ae47be48f530892362516acf7bda Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 25 Apr 2022 12:43:36 -0400 Subject: [PATCH 101/257] correct string example for E501 exclusions https://github.com/PyCQA/pycodestyle/issues/1064 --- docs/source/user/options.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index 5eb8e35..ab333c8 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -568,9 +568,9 @@ Options and their Descriptions # https://some-super-long-domain-name.com/with/some/very/long/path - url = ( - 'https://...' - ) + url = '''\ + https://... + ''' This defaults to: ``79`` From 50c60d3d2a698bd053f2cb320e56ca056e64ffe3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 21:59:09 +0000 Subject: [PATCH 102/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.0.1 → v3.1.0](https://github.com/asottile/reorder_python_imports/compare/v3.0.1...v3.1.0) - [github.com/pre-commit/mirrors-mypy: v0.942 → v0.950](https://github.com/pre-commit/mirrors-mypy/compare/v0.942...v0.950) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5955077..290a3b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.0.1 + rev: v3.1.0 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] @@ -27,7 +27,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.950 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 647eafbfd52d238ed56ba12fd8f4c177643aa52e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 22:07:33 +0000 Subject: [PATCH 103/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.32.0 → v2.32.1](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.32.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 290a3b9..871f223 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py36-plus] From f17f77c86d6a77c425eaadb325b023283c1808e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 22:11:03 +0000 Subject: [PATCH 104/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.950 → v0.960](https://github.com/pre-commit/mirrors-mypy/compare/v0.950...v0.960) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 871f223..4d40481 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.950 + rev: v0.960 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 9afcda128606a20c34e5bb7b57ffef79367107d6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 May 2022 18:24:13 -0400 Subject: [PATCH 105/257] fix docs build for sphinx 5.x --- docs/source/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 54d837c..e0406c2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -72,7 +72,7 @@ rst_epilog = """ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -297,8 +297,8 @@ texinfo_documents = [ intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} extlinks = { - "issue": ("https://github.com/pycqa/flake8/issues/%s", "#"), - "pull": ("https://github.com/pycqa/flake8/pull/%s", "#"), + "issue": ("https://github.com/pycqa/flake8/issues/%s", "#%s"), + "pull": ("https://github.com/pycqa/flake8/pull/%s", "#%s"), } autodoc_typehints = "description" From df19ff0def6f762754f4c25ae0d1ff87cba0ae1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 21:58:56 +0000 Subject: [PATCH 106/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.32.1 → v2.33.0](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.33.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d40481..5959ae4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.33.0 hooks: - id: pyupgrade args: [--py36-plus] From 7a6a0c856e946dffdd94564a22037b675c5aa25b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:05:52 +0000 Subject: [PATCH 107/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) - [github.com/asottile/pyupgrade: v2.33.0 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.33.0...v2.34.0) - [github.com/pre-commit/mirrors-mypy: v0.960 → v0.961](https://github.com/pre-commit/mirrors-mypy/compare/v0.960...v0.961) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5959ae4..430602b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: check-yaml - id: debug-statements @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.33.0 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -27,7 +27,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.960 + rev: v0.961 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 367c810f0ebbca39146a037d3b6f82f139c9ebab Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 24 Jun 2022 17:55:41 -0400 Subject: [PATCH 108/257] don't consider default codes as explicitly selected unless listed --- src/flake8/style_guide.py | 38 ++++++++++++++++++------------ tests/unit/test_decision_engine.py | 17 +++++++++++++ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index d9f6dbc..0504413 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -47,6 +47,15 @@ class Decision(enum.Enum): Selected = "selected error" +def _explicitly_chosen( + *, + option: Optional[List[str]], + extend: Optional[List[str]], +) -> Tuple[str, ...]: + ret = [*(option or []), *(extend or [])] + return tuple(sorted(ret, reverse=True)) + + def _select_ignore( *, option: Optional[List[str]], @@ -73,11 +82,13 @@ class DecisionEngine: """Initialize the engine.""" self.cache: Dict[str, Decision] = {} - self.using_default_select = ( - options.select is None and options.extend_select is None + self.selected_explicitly = _explicitly_chosen( + option=options.select, + extend=options.extend_select, ) - self.using_default_ignore = ( - options.ignore is None and options.extend_ignore is None + self.ignored_explicitly = _explicitly_chosen( + option=options.ignore, + extend=options.extend_ignore, ) self.selected = _select_ignore( @@ -86,7 +97,6 @@ class DecisionEngine: extended_default=options.extended_default_select, extend=options.extend_select, ) - self.ignored = _select_ignore( option=options.ignore, default=defaults.IGNORE, @@ -105,11 +115,10 @@ class DecisionEngine: Ignored.Implicitly if the selected list is not empty but no match was found. """ - if code.startswith(self.selected): - if self.using_default_select: - return Selected.Implicitly - else: - return Selected.Explicitly + if code.startswith(self.selected_explicitly): + return Selected.Explicitly + elif code.startswith(self.selected): + return Selected.Implicitly else: return Ignored.Implicitly @@ -125,11 +134,10 @@ class DecisionEngine: Selected.Implicitly if the ignored list is not empty but no match was found. """ - if code.startswith(self.ignored): - if self.using_default_ignore: - return Ignored.Implicitly - else: - return Ignored.Explicitly + if code.startswith(self.ignored_explicitly): + return Ignored.Explicitly + elif code.startswith(self.ignored): + return Ignored.Implicitly else: return Selected.Implicitly diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index 1280385..59c372a 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -224,3 +224,20 @@ def test_user_can_extend_select_to_enable_plugin_default_ignored(): ) assert decider.decision_for("P002") is style_guide.Decision.Selected + + +def test_plugin_extends_default_ignore_but_extend_selected(): + decider = style_guide.DecisionEngine( + create_options( + # user options --extend-select P002 --extend-ignore E501 + select=None, + ignore=None, + extend_select=["P002"], + extend_ignore=["E501"], + # a plugin is installed and extends default ignore + extended_default_select=["P"], + extended_default_ignore=["P002"], + ), + ) + + assert decider.decision_for("P002") is style_guide.Decision.Selected From ece6e2d1a8a9c7f23e7f30dc20723d87412c96b6 Mon Sep 17 00:00:00 2001 From: Stavros Ntentos <133706+stdedos@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:35:53 +0300 Subject: [PATCH 109/257] Add Discord invite badge to README.rst Signed-off-by: Stavros Ntentos <133706+stdedos@users.noreply.github.com> --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 23102af..f2a6d43 100644 --- a/README.rst +++ b/README.rst @@ -6,6 +6,10 @@ :target: https://results.pre-commit.ci/latest/github/PyCQA/flake8/main :alt: pre-commit.ci status +.. image:: https://img.shields.io/discord/825463413634891776.svg + :target: https://discord.gg/qYxpadCgkx + :alt: Discord + ======== Flake8 ======== From 7c10ae1994d31c0011fe38914b4324e1dd04ca65 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Jul 2022 00:16:44 +0000 Subject: [PATCH 110/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.1.0 → v3.3.0](https://github.com/asottile/reorder_python_imports/compare/v3.1.0...v3.3.0) - [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 430602b..773876c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + rev: v3.3.0 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] @@ -18,7 +18,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: [--line-length=79] From 2bb7d25ac769fc5688d694e36c2802a7079b08d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 23:19:09 +0000 Subject: [PATCH 111/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.3.0 → v3.8.1](https://github.com/asottile/reorder_python_imports/compare/v3.3.0...v3.8.1) - [github.com/asottile/pyupgrade: v2.34.0 → v2.37.1](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 773876c..1b3fb6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,12 +8,12 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.3.0 + rev: v3.8.1 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.1 hooks: - id: pyupgrade args: [--py36-plus] From 14a91d995cd3c5a03e78184f9d3e097dc45d3518 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 13 Jul 2022 15:40:40 -0400 Subject: [PATCH 112/257] ignore config files in the home directory --- src/flake8/options/config.py | 15 ++++++++++++++- tests/unit/test_options_config.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index 9756e7d..f5c93ce 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -14,7 +14,18 @@ from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) +def _stat_key(s: str) -> Tuple[int, int]: + # same as what's used by samefile / samestat + st = os.stat(s) + return st.st_ino, st.st_dev + + def _find_config_file(path: str) -> Optional[str]: + # on windows if the homedir isn't detected this returns back `~` + home = os.path.expanduser("~") + home_stat = _stat_key(home) if home != "~" else None + + dir_stat = _stat_key(path) cfg = configparser.RawConfigParser() while True: for candidate in ("setup.cfg", "tox.ini", ".flake8"): @@ -29,10 +40,12 @@ def _find_config_file(path: str) -> Optional[str]: return cfg_path new_path = os.path.dirname(path) - if new_path == path: + new_dir_stat = _stat_key(new_path) + if new_dir_stat == dir_stat or new_dir_stat == home_stat: break else: path = new_path + dir_stat = new_dir_stat # did not find any configuration file return None diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 02dd218..7a7c4f7 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -1,4 +1,6 @@ import configparser +import os.path +from unittest import mock import pytest @@ -66,6 +68,16 @@ def test_find_config_searches_upwards(tmp_path): assert config._find_config_file(str(subdir)) == str(expected) +def test_find_config_ignores_homedir(tmp_path): + subdir = tmp_path.joinpath("d") + subdir.mkdir() + + tmp_path.joinpath(".flake8").write_text("[flake8]") + + with mock.patch.object(os.path, "expanduser", return_value=str(tmp_path)): + assert config._find_config_file(str(subdir)) is None + + def test_load_config_config_specified_skips_discovery(tmpdir): tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") custom_cfg = tmpdir.join("custom.cfg") From 858c6d416cc8b7305b4b0a7f02b14ab80fdafb83 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 23:42:59 +0000 Subject: [PATCH 113/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.1 → v3.8.2](https://github.com/asottile/reorder_python_imports/compare/v3.8.1...v3.8.2) - [github.com/asottile/pyupgrade: v2.37.1 → v2.37.2](https://github.com/asottile/pyupgrade/compare/v2.37.1...v2.37.2) - [github.com/pre-commit/mirrors-mypy: v0.961 → v0.971](https://github.com/pre-commit/mirrors-mypy/compare/v0.961...v0.971) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b3fb6f..016f736 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,12 +8,12 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.1 + rev: v3.8.2 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.1 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py36-plus] @@ -27,7 +27,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.961 + rev: v0.971 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From d891612d78dbdcf5ef0550048689ff16d881539c Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Wed, 27 Jul 2022 22:17:54 +0200 Subject: [PATCH 114/257] Fix a typo --- src/flake8/checker.py | 2 +- src/flake8/main/application.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 2bbd6f2..00cff39 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) SERIAL_RETRY_ERRNOS = { # ENOSPC: Added by sigmavirus24 # > On some operating systems (OSX), multiprocessing may cause an - # > ENOSPC error while trying to trying to create a Semaphore. + # > ENOSPC error while trying to create a Semaphore. # > In those cases, we should replace the customized Queue Report # > class with pep8's StandardReport class to ensure users don't run # > into this problem. diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 111ed0c..15c2477 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -172,7 +172,7 @@ class Application: if parse_options is None: continue - # XXX: ideally we would't have two forms of parse_options + # XXX: ideally we wouldn't have two forms of parse_options try: parse_options( self.option_manager, From b9e0c6eb50e870f569602b4013e1c59cbe72bf2c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Jul 2022 13:35:02 -0400 Subject: [PATCH 115/257] upgrade pyflakes to 2.5.x --- docs/source/user/configuration.rst | 5 +---- docs/source/user/error-codes.rst | 2 -- setup.cfg | 2 +- src/flake8/plugins/pyflakes.py | 2 -- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/docs/source/user/configuration.rst b/docs/source/user/configuration.rst index f26674b..eca2667 100644 --- a/docs/source/user/configuration.rst +++ b/docs/source/user/configuration.rst @@ -180,7 +180,6 @@ look at a portion of a project's Flake8 configuration in their ``tox.ini``: [flake8] # it's not a bug that we aren't using all of hacking, ignore: - # F812: list comprehension redefines ... # H101: Use TODO(NAME) # H202: assertRaises Exception too broad # H233: Python 3.x incompatible use of print operator @@ -191,7 +190,7 @@ look at a portion of a project's Flake8 configuration in their ``tox.ini``: # H404: multi line docstring should start without a leading new line # H405: multi line docstring summary not separated with an empty line # H501: Do not use self.__dict__ for string formatting - ignore = F812,H101,H202,H233,H301,H306,H401,H403,H404,H405,H501 + ignore = H101,H202,H233,H301,H306,H401,H403,H404,H405,H501 They use the comments to describe the check but they could also write this as: @@ -200,8 +199,6 @@ They use the comments to describe the check but they could also write this as: [flake8] # it's not a bug that we aren't using all of hacking ignore = - # F812: list comprehension redefines ... - F812, # H101: Use TODO(NAME) H101, # H202: assertRaises Exception too broad diff --git a/docs/source/user/error-codes.rst b/docs/source/user/error-codes.rst index 9eab3f7..d12a07f 100644 --- a/docs/source/user/error-codes.rst +++ b/docs/source/user/error-codes.rst @@ -85,8 +85,6 @@ generates its own :term:`error code`\ s for ``pyflakes``: +------+---------------------------------------------------------------------+ | F704 | a ``yield`` or ``yield from`` statement outside of a function | +------+---------------------------------------------------------------------+ -| F705 | a ``return`` statement with arguments inside a generator | -+------+---------------------------------------------------------------------+ | F706 | a ``return`` statement outside of a function/method | +------+---------------------------------------------------------------------+ | F707 | an ``except:`` block as not the last exception handler | diff --git a/setup.cfg b/setup.cfg index acb152a..4e27351 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,7 @@ package_dir = install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.8.0,<2.9.0 - pyflakes>=2.4.0,<2.5.0 + pyflakes>=2.5.0,<2.6.0 importlib-metadata<4.3;python_version<"3.8" python_requires = >=3.6.1 diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index dc8a02e..54eaeca 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -49,14 +49,12 @@ FLAKE8_PYFLAKES_CODES = { "ContinueOutsideLoop": "F702", "ContinueInFinally": "F703", "YieldOutsideFunction": "F704", - "ReturnWithArgsInsideGenerator": "F705", "ReturnOutsideFunction": "F706", "DefaultExceptNotLast": "F707", "DoctestSyntaxError": "F721", "ForwardAnnotationSyntaxError": "F722", "CommentAnnotationSyntaxError": "F723", "RedefinedWhileUnused": "F811", - "RedefinedInListComp": "F812", "UndefinedName": "F821", "UndefinedExport": "F822", "UndefinedLocal": "F823", From 7838f1191c684795ce631a33fa7dc44b43e5f164 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Jul 2022 15:37:12 -0400 Subject: [PATCH 116/257] upgrade pycodestyle to 2.9.x --- setup.cfg | 2 +- src/flake8/plugins/pycodestyle.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4e27351..0978085 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,7 @@ package_dir = # https://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.8.0,<2.9.0 + pycodestyle>=2.9.0,<2.10.0 pyflakes>=2.5.0,<2.6.0 importlib-metadata<4.3;python_version<"3.8" python_requires = >=3.6.1 diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py index e8011b2..753af23 100644 --- a/src/flake8/plugins/pycodestyle.py +++ b/src/flake8/plugins/pycodestyle.py @@ -21,7 +21,7 @@ from pycodestyle import indentation as _indentation from pycodestyle import maximum_doc_length as _maximum_doc_length from pycodestyle import maximum_line_length as _maximum_line_length from pycodestyle import missing_whitespace as _missing_whitespace -from pycodestyle import missing_whitespace_after_import_keyword as _missing_whitespace_after_import_keyword # noqa: E501 +from pycodestyle import missing_whitespace_after_keyword as _missing_whitespace_after_keyword # noqa: E501 from pycodestyle import missing_whitespace_around_operator as _missing_whitespace_around_operator # noqa: E501 from pycodestyle import module_imports_on_top_of_file as _module_imports_on_top_of_file # noqa: E501 from pycodestyle import python_3000_async_await_keywords as _python_3000_async_await_keywords # noqa: E501 @@ -78,7 +78,7 @@ def pycodestyle_logical( yield from _indentation(logical_line, previous_logical, indent_char, indent_level, previous_indent_level, indent_size) # noqa: E501 yield from _maximum_doc_length(logical_line, max_doc_length, noqa, tokens) yield from _missing_whitespace(logical_line) - yield from _missing_whitespace_after_import_keyword(logical_line) + yield from _missing_whitespace_after_keyword(logical_line, tokens) yield from _missing_whitespace_around_operator(logical_line, tokens) yield from _module_imports_on_top_of_file(logical_line, indent_level, checker_state, noqa) # noqa: E501 yield from _python_3000_async_await_keywords(logical_line, tokens) From c7c6218e58f0fc005bd80f1872da63cd51afe71a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Jul 2022 17:00:41 -0400 Subject: [PATCH 117/257] Release 5.0.0 --- docs/source/internal/releases.rst | 4 +- docs/source/release-notes/5.0.0.rst | 76 +++++++++++++++++++++++++++++ docs/source/release-notes/index.rst | 6 +++ src/flake8/__init__.py | 2 +- src/flake8/plugins/finder.py | 2 +- tests/unit/plugins/finder_test.py | 2 +- 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 docs/source/release-notes/5.0.0.rst diff --git a/docs/source/internal/releases.rst b/docs/source/internal/releases.rst index 1107c98..f15ea0c 100644 --- a/docs/source/internal/releases.rst +++ b/docs/source/internal/releases.rst @@ -28,9 +28,9 @@ Historically, |Flake8| has generated major releases for: - Unvendoring dependencies (2.0) -- Large scale refactoring (2.0, 3.0) +- Large scale refactoring (2.0, 3.0, 5.0) -- Subtly breaking CLI changes (3.0, 4.0) +- Subtly breaking CLI changes (3.0, 4.0, 5.0) - Breaking changes to its plugin interface (3.0) diff --git a/docs/source/release-notes/5.0.0.rst b/docs/source/release-notes/5.0.0.rst new file mode 100644 index 0000000..e18efc4 --- /dev/null +++ b/docs/source/release-notes/5.0.0.rst @@ -0,0 +1,76 @@ +5.0.0 -- 2022-07-30 +------------------- + +You can view the `5.0.0 milestone`_ on GitHub for more details. + +Backwards Incompatible Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove ``indent_size_str`` (See also :pull:`1411`). +- Remove some dead code (See also :pull:`1453`, :pull:`1540`, :pull:`1541`). +- Missing explicitly-specified configuration is now an error (See also + :issue:`1497`, :pull:`1498`). +- Always read configuration files as UTF-8 (See also :issue:`1532`, + :pull:`1533`). +- Remove manpage from docs -- use ``help2man`` or related tools instead (See + also :pull:`1557`). +- Forbid invalid plugin codes (See also :issue:`325`, :pull:`1579`). + + +Deprecations +~~~~~~~~~~~~ + +- Deprecate ``--diff`` option (See also :issue:`1389`, :pull:`1441`). + + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.9.0, < 2.10.0 (See also :pull:`1626`). +- Pyflakes has been updated to >= 2.5.0, < 2.6.0 (See also :pull:`1625`). +- mccabe has been updated to >= 0.7.0, < 0.8.0 (See also :pull:`1542`). + + +Features +~~~~~~~~ + +- Add colors to output, configurable via ``--color`` (See also :issue:`1379`, + :pull:`1440`). +- Add ``.nox`` to the default exclude list (See also :issue:`1442`, + :pull:`1443`). +- Don't consider a config file which does not contain flake8 settings (See + also :issue:`199`, :pull:`1472`). +- Duplicate ``local-plugins`` names are now allowed (See also :issue:`362`, + :pull:`1504`). +- Consider ``.`` to be a path in config files (See also :issue:`1494`, + :pull:`1508`) +- Add ``--require-plugins`` option taking distribution names (See also + :issue:`283`, :pull:`1535`). +- Improve performance by removing debug logs (See also :pull:`1537`, + :pull:`1544`). +- Include failing file path in plugin execution error (See also :issue:`265`, + :pull:`1543`). +- Improve performance by pre-generating a ``pycodestyle`` plugin (See also + :pull:`1545`). +- Properly differentiate between explicitly ignored / selected and default + ignored / selected options (See also :issue:`284`, :pull:`1576`, + :pull:`1609`). + + +Bugs Fixed +~~~~~~~~~~ + +- Fix physical line plugins not receiving all lines in the case of + triple-quoted strings (See also :issue:`1534`, :pull:`1536`). +- Fix duplicate error logging in the case of plugin issues (See also + :pull:`1538`). +- Fix inconsistent ordering of ``--ignore`` in ``--help`` (See also + :issue:`1550`, :pull:`1552`). +- Fix memory leak of style guides by avoiding ``@lru_cache`` of a method (See + also :pull:`1573`). +- Fix ignoring of configuration files exactly in the home directory (See also + :issue:`1617`, :pull:`1618`). + +.. all links +.. _5.0.0 milestone: + https://github.com/PyCQA/flake8/milestone/42 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index c5d20a3..b101a57 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -5,6 +5,12 @@ All of the release notes that have been recorded for Flake8 are organized here with the newest releases first. +5.x Release Series +================== + +.. toctree:: + 5.0.0 + 4.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index aef6354..b0ccfc8 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ from typing import Type LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "4.0.1" +__version__ = "5.0.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 3c576e9..fb87d0d 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -27,7 +27,7 @@ VALID_CODE = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII) FLAKE8_GROUPS = frozenset(("flake8.extension", "flake8.report")) BANNED_PLUGINS = { - "flake8-colors": "4.1", + "flake8-colors": "5.0", "flake8-per-file-ignores": "3.7", } diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 91cb11f..3c11c64 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -356,7 +356,7 @@ unrelated = unrelated:main ( "flake8.plugins.finder", 30, - "flake8-colors plugin is obsolete in flake8>=4.1", + "flake8-colors plugin is obsolete in flake8>=5.0", ), ] From b0cad5530eac3ebe007141aae7295e198956a3b1 Mon Sep 17 00:00:00 2001 From: Max R Date: Sun, 31 Jul 2022 07:37:54 -0400 Subject: [PATCH 118/257] Remove needless sort in `_style_guide_for` We are always returning the last element so a 'max' operation is sufficient instead of sorting. Note the old code did not handle an empty list so this change doesn't either --- src/flake8/style_guide.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 0504413..b7115a0 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -254,13 +254,10 @@ class StyleGuideManager: def _style_guide_for(self, filename: str) -> "StyleGuide": """Find the StyleGuide for the filename in particular.""" - guides = sorted( + return max( (g for g in self.style_guides if g.applies_to(filename)), key=lambda g: len(g.filename or ""), ) - if len(guides) > 1: - return guides[-1] - return guides[0] @contextlib.contextmanager def processing_file( From fce93b952a423a53c204c12aa0a20eede6948758 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 Jul 2022 18:01:12 -0400 Subject: [PATCH 119/257] prevent duplicate plugin discovery on misconfigured pythons for example, `venv`-virtualenvs on fedora have both `lib` and `lib64` on `sys.path` despite them being the same. this causes `importlib.metadata.distributions` to double-discover. ```console $ docker run --rm -t fedora:latest bash -c 'dnf install -qq -y python3 >& /dev/null && python3 -m venv venv && venv/bin/pip -qq install cfgv && venv/bin/python - <<< "from importlib.metadata import distributions; print(len([d for d in distributions() if d.name == '"'"'cfgv'"'"']))"' 2 ``` --- src/flake8/plugins/finder.py | 7 +++++++ tests/unit/plugins/finder_test.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index fb87d0d..9e9e3af 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -179,6 +179,8 @@ def _flake8_plugins( def _find_importlib_plugins() -> Generator[Plugin, None, None]: + # some misconfigured pythons (RHEL) have things on `sys.path` twice + seen = set() for dist in importlib_metadata.distributions(): # assigned to prevent continual reparsing eps = dist.entry_points @@ -190,6 +192,11 @@ def _find_importlib_plugins() -> Generator[Plugin, None, None]: # assigned to prevent continual reparsing meta = dist.metadata + if meta["name"] in seen: + continue + else: + seen.add(meta["name"]) + if meta["name"] in BANNED_PLUGINS: LOG.warning( "%s plugin is obsolete in flake8>=%s", diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 3c11c64..63f8156 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -361,6 +361,23 @@ unrelated = unrelated:main ] +def test_duplicate_dists(flake8_dist): + # some poorly packaged pythons put lib and lib64 on sys.path resulting in + # duplicates from `importlib.metadata.distributions` + with mock.patch.object( + importlib_metadata, + "distributions", + return_value=[ + flake8_dist, + flake8_dist, + ], + ): + ret = list(finder._find_importlib_plugins()) + + # we should not have duplicates + assert len(ret) == len(set(ret)) + + def test_find_local_plugins_nothing(): cfg = configparser.RawConfigParser() assert set(finder._find_local_plugins(cfg)) == set() From 405cfe06e02722a816068bf0c50c5947533ad602 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 Jul 2022 18:10:18 -0400 Subject: [PATCH 120/257] Release 5.0.1 --- docs/source/release-notes/5.0.1.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/5.0.1.rst diff --git a/docs/source/release-notes/5.0.1.rst b/docs/source/release-notes/5.0.1.rst new file mode 100644 index 0000000..89b6bf6 --- /dev/null +++ b/docs/source/release-notes/5.0.1.rst @@ -0,0 +1,15 @@ +5.0.1 -- 2022-07-31 +------------------- + +You can view the `5.0.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix duplicate plugin discovery on misconfigured pythons (See also + :issue:`1627`, :pull:`1631`). + + +.. all links +.. _5.0.1 milestone: + https://github.com/PyCQA/flake8/milestone/43 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index b101a57..b44fd27 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -10,6 +10,7 @@ with the newest releases first. .. toctree:: 5.0.0 + 5.0.1 4.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index b0ccfc8..04c4c62 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ from typing import Type LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "5.0.0" +__version__ = "5.0.1" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 91a7fa9ac360be7f43b1325875cf5bf4fe89e380 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 Jul 2022 21:54:32 -0400 Subject: [PATCH 121/257] fix order of release notes --- docs/source/release-notes/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index b44fd27..aff0733 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -9,8 +9,8 @@ with the newest releases first. ================== .. toctree:: - 5.0.0 5.0.1 + 5.0.0 4.x Release Series ================== From b70d7a2f7d3119569be5ae597863a810e0422bd2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Aug 2022 05:37:47 -0700 Subject: [PATCH 122/257] work around un-pickleabiliy of EntryPoint in 3.8.0 --- src/flake8/_compat.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py index 18809e2..81da7be 100644 --- a/src/flake8/_compat.py +++ b/src/flake8/_compat.py @@ -6,4 +6,11 @@ if sys.version_info >= (3, 8): # pragma: no cover (PY38+) else: # pragma: no cover ( Date: Mon, 1 Aug 2022 05:51:38 -0700 Subject: [PATCH 123/257] skip skipping home if home does not exist --- src/flake8/options/config.py | 5 ++++- tests/unit/test_options_config.py | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index f5c93ce..36fe976 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -23,7 +23,10 @@ def _stat_key(s: str) -> Tuple[int, int]: def _find_config_file(path: str) -> Optional[str]: # on windows if the homedir isn't detected this returns back `~` home = os.path.expanduser("~") - home_stat = _stat_key(home) if home != "~" else None + try: + home_stat = _stat_key(home) if home != "~" else None + except OSError: # FileNotFoundError / PermissionError / etc. + home_stat = None dir_stat = _stat_key(path) cfg = configparser.RawConfigParser() diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 7a7c4f7..bdc2208 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -78,6 +78,13 @@ def test_find_config_ignores_homedir(tmp_path): assert config._find_config_file(str(subdir)) is None +def test_find_config_ignores_unknown_homedir(tmp_path): + subdir = tmp_path.joinpath("d") + + with mock.patch.object(os.path, "expanduser", return_value=str(subdir)): + assert config._find_config_file(str(tmp_path)) is None + + def test_load_config_config_specified_skips_discovery(tmpdir): tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") custom_cfg = tmpdir.join("custom.cfg") From 70c0b3d27a5626a6ad58293bfaa6308244cb349c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Aug 2022 06:01:45 -0700 Subject: [PATCH 124/257] Release 5.0.2 --- docs/source/release-notes/5.0.2.rst | 16 ++++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/5.0.2.rst diff --git a/docs/source/release-notes/5.0.2.rst b/docs/source/release-notes/5.0.2.rst new file mode 100644 index 0000000..322d202 --- /dev/null +++ b/docs/source/release-notes/5.0.2.rst @@ -0,0 +1,16 @@ +5.0.2 -- 2022-08-01 +------------------- + +You can view the `5.0.2 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix execution on python == 3.8.0 (See also :issue:`1637`, :pull:`1641`). +- Fix config discovery when home does not exist (See also :issue:`1640`, + :pull:`1642`). + + +.. all links +.. _5.0.2 milestone: + https://github.com/PyCQA/flake8/milestone/44 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index aff0733..7a0e10f 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -9,6 +9,7 @@ with the newest releases first. ================== .. toctree:: + 5.0.2 5.0.1 5.0.0 diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 04c4c62..8a50d29 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ from typing import Type LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "5.0.1" +__version__ = "5.0.2" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 25e8ff18b30b58f1dabc1d20546ebc20fd775560 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Aug 2022 19:11:53 -0400 Subject: [PATCH 125/257] ignore config files that partially parse as flake8 configs --- src/flake8/options/config.py | 2 +- tests/unit/test_options_config.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index 36fe976..daf8529 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -29,9 +29,9 @@ def _find_config_file(path: str) -> Optional[str]: home_stat = None dir_stat = _stat_key(path) - cfg = configparser.RawConfigParser() while True: for candidate in ("setup.cfg", "tox.ini", ".flake8"): + cfg = configparser.RawConfigParser() cfg_path = os.path.join(path, candidate) try: cfg.read(cfg_path, encoding="UTF-8") diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index bdc2208..4ad6acd 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -21,7 +21,9 @@ def test_config_file_without_section_is_not_considered(tmp_path): def test_config_file_with_parse_error_is_not_considered(tmp_path, caplog): - tmp_path.joinpath("setup.cfg").write_text("[error") + # the syntax error here is deliberately to trigger a partial parse + # https://github.com/python/cpython/issues/95546 + tmp_path.joinpath("setup.cfg").write_text("[flake8]\nx = 1\n...") assert config._find_config_file(str(tmp_path)) is None From ff6569b87db8ae28c41b548071454de620ad14d5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Aug 2022 19:21:10 -0400 Subject: [PATCH 126/257] Release 5.0.3 --- docs/source/release-notes/5.0.3.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/5.0.3.rst diff --git a/docs/source/release-notes/5.0.3.rst b/docs/source/release-notes/5.0.3.rst new file mode 100644 index 0000000..25f8d93 --- /dev/null +++ b/docs/source/release-notes/5.0.3.rst @@ -0,0 +1,15 @@ +5.0.3 -- 2022-08-01 +------------------- + +You can view the `5.0.3 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Work around partial reads of configuration files with syntax errors (See + also :issue:`1647`, :pull:`1648`). + + +.. all links +.. _5.0.3 milestone: + https://github.com/PyCQA/flake8/milestone/45 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 7a0e10f..aaeb4c0 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -9,6 +9,7 @@ with the newest releases first. ================== .. toctree:: + 5.0.3 5.0.2 5.0.1 5.0.0 diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 8a50d29..26a8d5b 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ from typing import Type LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "5.0.2" +__version__ = "5.0.3" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 84d56a8c25106b5e0a41cdf63b5de261f8da5c99 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 01:01:58 +0000 Subject: [PATCH 127/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.37.2 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.37.2...v2.37.3) - [github.com/PyCQA/flake8: 4.0.1 → 5.0.3](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.3) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 016f736..07a863e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: reorder-python-imports args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.2 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py36-plus] @@ -23,7 +23,7 @@ repos: - id: black args: [--line-length=79] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.3 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 7b8b374c9bc1a141ca7cf6670c8cee6708398490 Mon Sep 17 00:00:00 2001 From: Todd Levi Date: Mon, 1 Aug 2022 13:11:02 -0500 Subject: [PATCH 128/257] Clarify entry point naming Clarified what is and is not a valid entry point name for registering plugins. --- .../plugin-development/registering-plugins.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index b9e87fb..065bdd8 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -112,11 +112,17 @@ look like:: X101 = flake8_example:ExamplePlugin +In the above case, the entry-point name and the error code produced by your +plugin are the same. + If your plugin reports several error codes that all start with ``X10``, then it would look like:: X10 = flake8_example:ExamplePlugin +In this casae as well as the following case, your entry-point name acts as +a prefix to the error codes produced by your plugin. + If all of your plugin's error codes start with ``X1`` then it would look like:: @@ -130,8 +136,12 @@ in the users environment. Selecting an entry point that is already used can cause plugins to be deactivated without warning! **Please Note:** Your entry point does not need to be exactly 4 characters -as of |Flake8| 3.0. *Consider using an entry point with 3 letters followed -by 3 numbers (i.e.* ``ABC123`` *).* +as of |Flake8| 3.0. Single letter entry point prefixes (such as the +'X' in the examples above) have caused issues in the past. As such, +please consider using a 2 or 3 character entry point prefix, +i.e., ``ABC`` is better than ``A`` but ``ABCD`` is invalid. +*A 3 letters entry point prefix followed by 3 numbers (i.e.* ``ABC123`` *) +is currently the longest allowed entry point name.* .. _Entry Points: From e94ee2b5f1801354b940cfe830b9160852915aec Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 3 Aug 2022 18:49:44 -0400 Subject: [PATCH 129/257] require sufficiently new importlib-metadata --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0978085..5c7b2d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.9.0,<2.10.0 pyflakes>=2.5.0,<2.6.0 - importlib-metadata<4.3;python_version<"3.8" + importlib-metadata>=1.1.0,<4.3;python_version<"3.8" python_requires = >=3.6.1 [options.packages.find] From 6027577d325b0dd8bf1e465ebd29b71b5f0d005b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 3 Aug 2022 19:19:37 -0400 Subject: [PATCH 130/257] Release 5.0.4 --- docs/source/release-notes/5.0.4.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/5.0.4.rst diff --git a/docs/source/release-notes/5.0.4.rst b/docs/source/release-notes/5.0.4.rst new file mode 100644 index 0000000..b5c4889 --- /dev/null +++ b/docs/source/release-notes/5.0.4.rst @@ -0,0 +1,15 @@ +5.0.4 -- 2022-08-03 +------------------- + +You can view the `5.0.4 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Set a lower bound on ``importlib-metadata`` to prevent ``RecursionError`` + (See also :issue:`1650`, :pull:`1653`). + + +.. all links +.. _5.0.4 milestone: + https://github.com/PyCQA/flake8/milestone/46 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index aaeb4c0..d511f30 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -9,6 +9,7 @@ with the newest releases first. ================== .. toctree:: + 5.0.4 5.0.3 5.0.2 5.0.1 diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 26a8d5b..ea571c9 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ from typing import Type LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "5.0.3" +__version__ = "5.0.4" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From e94fb1094070ccf4b4fa44468cf5dde08bda68c7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 Jul 2022 19:38:54 -0400 Subject: [PATCH 131/257] require python>=3.7 --- .github/workflows/main.yml | 5 +- .pre-commit-config.yaml | 8 +- bin/gen-pycodestyle-plugin | 14 ++-- docs/source/conf.py | 2 + example-plugin/setup.py | 2 + .../src/flake8_example_plugin/__init__.py | 2 + .../flake8_example_plugin/off_by_default.py | 1 + .../flake8_example_plugin/on_by_default.py | 1 + setup.cfg | 3 +- setup.py | 2 + src/flake8/__init__.py | 8 +- src/flake8/__main__.py | 2 + src/flake8/_compat.py | 2 + src/flake8/api/__init__.py | 1 + src/flake8/api/legacy.py | 21 +++-- src/flake8/checker.py | 31 ++++---- src/flake8/defaults.py | 2 + src/flake8/discover_files.py | 2 + src/flake8/exceptions.py | 1 + src/flake8/formatting/__init__.py | 1 + src/flake8/formatting/_windows_color.py | 2 + src/flake8/formatting/base.py | 19 +++-- src/flake8/formatting/default.py | 15 ++-- src/flake8/main/__init__.py | 1 + src/flake8/main/application.py | 33 ++++---- src/flake8/main/cli.py | 5 +- src/flake8/main/debug.py | 5 +- src/flake8/main/options.py | 2 + src/flake8/options/__init__.py | 1 + src/flake8/options/aggregator.py | 5 +- src/flake8/options/config.py | 18 ++--- src/flake8/options/manager.py | 76 +++++++++---------- src/flake8/plugins/__init__.py | 1 + src/flake8/plugins/finder.py | 51 ++++++------- src/flake8/plugins/pycodestyle.py | 7 +- src/flake8/plugins/pyflakes.py | 13 ++-- src/flake8/plugins/reporter.py | 5 +- src/flake8/processor.py | 38 +++++----- src/flake8/statistics.py | 21 +++-- src/flake8/style_guide.py | 62 +++++++-------- src/flake8/utils.py | 32 ++++---- src/flake8/violation.py | 11 ++- tests/__init__.py | 1 + tests/conftest.py | 2 + tests/integration/subdir/aplugin.py | 1 + tests/integration/test_aggregator.py | 2 + tests/integration/test_api_legacy.py | 2 + tests/integration/test_checker.py | 2 + tests/integration/test_main.py | 2 + tests/integration/test_plugins.py | 2 + tests/unit/conftest.py | 2 + tests/unit/plugins/finder_test.py | 2 + tests/unit/plugins/pycodestyle_test.py | 2 + tests/unit/plugins/reporter_test.py | 2 + tests/unit/test_application.py | 2 + tests/unit/test_base_formatter.py | 2 + tests/unit/test_checker_manager.py | 2 + tests/unit/test_debug.py | 2 + tests/unit/test_decision_engine.py | 2 + tests/unit/test_discover_files.py | 2 + tests/unit/test_exceptions.py | 2 + tests/unit/test_file_checker.py | 2 + tests/unit/test_file_processor.py | 2 + tests/unit/test_filenameonly_formatter.py | 2 + tests/unit/test_legacy_api.py | 2 + tests/unit/test_main_options.py | 2 + tests/unit/test_nothing_formatter.py | 2 + tests/unit/test_option.py | 2 + tests/unit/test_option_manager.py | 2 + tests/unit/test_options_config.py | 2 + tests/unit/test_pyflakes_codes.py | 2 + tests/unit/test_statistics.py | 2 + tests/unit/test_style_guide.py | 2 + tests/unit/test_utils.py | 2 + tests/unit/test_violation.py | 2 + tox.ini | 2 +- 76 files changed, 337 insertions(+), 263 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c9b32d7..868b5ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,9 +15,6 @@ jobs: - os: ubuntu-latest python: pypy-3.7 toxenv: py - - os: ubuntu-latest - python: 3.6 - toxenv: py - os: ubuntu-latest python: 3.7 toxenv: py @@ -32,7 +29,7 @@ jobs: toxenv: py # windows - os: windows-latest - python: 3.6 + python: 3.7 toxenv: py # misc - os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07a863e..faecf55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,12 +11,16 @@ repos: rev: v3.8.2 hooks: - id: reorder-python-imports - args: [--application-directories, '.:src', --py36-plus] + args: [ + --application-directories, '.:src', + --py37-plus, + --add-import, 'from __future__ import annotations', + ] - repo: https://github.com/asottile/pyupgrade rev: v2.37.3 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py37-plus] - repo: https://github.com/psf/black rev: 22.6.0 hooks: diff --git a/bin/gen-pycodestyle-plugin b/bin/gen-pycodestyle-plugin index 3540a9a..8bc2efc 100755 --- a/bin/gen-pycodestyle-plugin +++ b/bin/gen-pycodestyle-plugin @@ -1,11 +1,12 @@ #!/usr/bin/env python3 +from __future__ import annotations + import inspect import os.path from typing import Any from typing import Callable from typing import Generator from typing import NamedTuple -from typing import Tuple import pycodestyle @@ -20,7 +21,7 @@ def _too_long(s: str) -> str: class Call(NamedTuple): name: str is_generator: bool - params: Tuple[str, ...] + params: tuple[str, ...] def to_src(self) -> str: params_s = ", ".join(self.params) @@ -35,7 +36,7 @@ class Call(NamedTuple): return "\n".join(lines) @classmethod - def from_func(cls, func: Callable[..., Any]) -> "Call": + def from_func(cls, func: Callable[..., Any]) -> Call: spec = inspect.getfullargspec(func) params = tuple(spec.args) return cls(func.__name__, inspect.isgeneratorfunction(func), params) @@ -55,9 +56,10 @@ def lines() -> Generator[str, None, None]: yield f'"""Generated using ./bin/{os.path.basename(__file__)}."""' yield "# fmt: off" + yield "from __future__ import annotations" + yield "" yield "from typing import Any" yield "from typing import Generator" - yield "from typing import Tuple" yield "" imports = sorted(call.name for call in logical + physical) for name in imports: @@ -69,7 +71,7 @@ def lines() -> Generator[str, None, None]: logical_params = {param for call in logical for param in call.params} for param in sorted(logical_params): yield f" {param}: Any," - yield ") -> Generator[Tuple[int, str], None, None]:" + yield ") -> Generator[tuple[int, str], None, None]:" yield ' """Run pycodestyle logical checks."""' for call in sorted(logical): yield call.to_src() @@ -80,7 +82,7 @@ def lines() -> Generator[str, None, None]: physical_params = {param for call in physical for param in call.params} for param in sorted(physical_params): yield f" {param}: Any," - yield ") -> Generator[Tuple[int, str], None, None]:" + yield ") -> Generator[tuple[int, str], None, None]:" yield ' """Run pycodestyle physical checks."""' for call in sorted(physical): yield call.to_src() diff --git a/docs/source/conf.py b/docs/source/conf.py index e0406c2..a2b4af3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,6 +14,8 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) +from __future__ import annotations + import flake8 # -- General configuration ------------------------------------------------ diff --git a/example-plugin/setup.py b/example-plugin/setup.py index 70d56fa..c0720bd 100644 --- a/example-plugin/setup.py +++ b/example-plugin/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import setuptools setuptools.setup( diff --git a/example-plugin/src/flake8_example_plugin/__init__.py b/example-plugin/src/flake8_example_plugin/__init__.py index 3f6f163..47851da 100644 --- a/example-plugin/src/flake8_example_plugin/__init__.py +++ b/example-plugin/src/flake8_example_plugin/__init__.py @@ -1,4 +1,6 @@ """Module for an example Flake8 plugin.""" +from __future__ import annotations + from .off_by_default import ExampleTwo from .on_by_default import ExampleOne diff --git a/example-plugin/src/flake8_example_plugin/off_by_default.py b/example-plugin/src/flake8_example_plugin/off_by_default.py index 54737cb..d140ca1 100644 --- a/example-plugin/src/flake8_example_plugin/off_by_default.py +++ b/example-plugin/src/flake8_example_plugin/off_by_default.py @@ -1,4 +1,5 @@ """Our first example plugin.""" +from __future__ import annotations class ExampleTwo: diff --git a/example-plugin/src/flake8_example_plugin/on_by_default.py b/example-plugin/src/flake8_example_plugin/on_by_default.py index a3e5332..d2da126 100644 --- a/example-plugin/src/flake8_example_plugin/on_by_default.py +++ b/example-plugin/src/flake8_example_plugin/on_by_default.py @@ -1,4 +1,5 @@ """Our first example plugin.""" +from __future__ import annotations class ExampleOne: diff --git a/setup.cfg b/setup.cfg index 5c7b2d3..5e20adf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -43,7 +42,7 @@ install_requires = pycodestyle>=2.9.0,<2.10.0 pyflakes>=2.5.0,<2.6.0 importlib-metadata>=1.1.0,<4.3;python_version<"3.8" -python_requires = >=3.6.1 +python_requires = >=3.7 [options.packages.find] where = src diff --git a/setup.py b/setup.py index 3822d9e..253a22e 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ """Packaging logic for Flake8.""" +from __future__ import annotations + import os import sys diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index ea571c9..43f3bec 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -9,10 +9,10 @@ This module .. autofunction:: flake8.configure_logging """ +from __future__ import annotations + import logging import sys -from typing import Optional -from typing import Type LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) @@ -35,7 +35,7 @@ LOG_FORMAT = ( def configure_logging( verbosity: int, - filename: Optional[str] = None, + filename: str | None = None, logformat: str = LOG_FORMAT, ) -> None: """Configure logging for flake8. @@ -56,7 +56,7 @@ def configure_logging( if not filename or filename in ("stderr", "stdout"): fileobj = getattr(sys, filename or "stderr") - handler_cls: Type[logging.Handler] = logging.StreamHandler + handler_cls: type[logging.Handler] = logging.StreamHandler else: fileobj = filename handler_cls = logging.FileHandler diff --git a/src/flake8/__main__.py b/src/flake8/__main__.py index de240dc..8f7e7c9 100644 --- a/src/flake8/__main__.py +++ b/src/flake8/__main__.py @@ -1,4 +1,6 @@ """Module allowing for ``python -m flake8 ...``.""" +from __future__ import annotations + from flake8.main.cli import main if __name__ == "__main__": diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py index 81da7be..91770bc 100644 --- a/src/flake8/_compat.py +++ b/src/flake8/_compat.py @@ -1,4 +1,6 @@ """Expose backports in a single place.""" +from __future__ import annotations + import sys if sys.version_info >= (3, 8): # pragma: no cover (PY38+) diff --git a/src/flake8/api/__init__.py b/src/flake8/api/__init__.py index c2eefbe..c5f9711 100644 --- a/src/flake8/api/__init__.py +++ b/src/flake8/api/__init__.py @@ -3,3 +3,4 @@ This is the only submodule in Flake8 with a guaranteed stable API. All other submodules are considered internal only and are subject to change. """ +from __future__ import annotations diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 5881aa9..9635756 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -3,13 +3,12 @@ Previously, users would import :func:`get_style_guide` from ``flake8.engine``. In 3.0 we no longer have an "engine" module but we maintain the API from it. """ +from __future__ import annotations + import argparse import logging import os.path from typing import Any -from typing import List -from typing import Optional -from typing import Type import flake8 from flake8.discover_files import expand_paths @@ -53,7 +52,7 @@ class Report: """Return the total number of errors.""" return self._application.result_count - def get_statistics(self, violation: str) -> List[str]: + def get_statistics(self, violation: str) -> list[str]: """Get the list of occurrences of a violation. :returns: @@ -97,12 +96,12 @@ class StyleGuide: return self._application.options @property - def paths(self) -> List[str]: + def paths(self) -> list[str]: """Return the extra arguments passed as paths.""" assert self._application.options is not None return self._application.options.filenames - def check_files(self, paths: Optional[List[str]] = None) -> Report: + def check_files(self, paths: list[str] | None = None) -> Report: """Run collected checks on the files provided. This will check the files passed in and return a :class:`Report` @@ -119,7 +118,7 @@ class StyleGuide: self._application.report_errors() return Report(self._application) - def excluded(self, filename: str, parent: Optional[str] = None) -> bool: + def excluded(self, filename: str, parent: str | None = None) -> bool: """Determine if a file is excluded. :param filename: @@ -148,7 +147,7 @@ class StyleGuide: def init_report( self, - reporter: Optional[Type[formatter.BaseFormatter]] = None, + reporter: type[formatter.BaseFormatter] | None = None, ) -> None: """Set up a formatter for this run of Flake8.""" if reporter is None: @@ -170,9 +169,9 @@ class StyleGuide: def input_file( self, filename: str, - lines: Optional[Any] = None, - expected: Optional[Any] = None, - line_offset: Optional[Any] = 0, + lines: Any | None = None, + expected: Any | None = None, + line_offset: Any | None = 0, ) -> Report: """Run collected checks on a single file. diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 00cff39..d73349f 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -1,4 +1,6 @@ """Checker Manager and Checker classes.""" +from __future__ import annotations + import argparse import collections import errno @@ -8,7 +10,6 @@ import multiprocessing.pool import signal import tokenize from typing import Any -from typing import Dict from typing import List from typing import Optional from typing import Tuple @@ -71,8 +72,8 @@ class Manager: self.options = style_guide.options self.plugins = plugins self.jobs = self._job_count() - self._all_checkers: List[FileChecker] = [] - self.checkers: List[FileChecker] = [] + self._all_checkers: list[FileChecker] = [] + self.checkers: list[FileChecker] = [] self.statistics = { "files": 0, "logical lines": 0, @@ -152,7 +153,7 @@ class Manager: ) return reported_results_count - def make_checkers(self, paths: Optional[List[str]] = None) -> None: + def make_checkers(self, paths: list[str] | None = None) -> None: """Create checkers for each file.""" if paths is None: paths = self.options.filenames @@ -174,7 +175,7 @@ class Manager: self.checkers = [c for c in self._all_checkers if c.should_process] LOG.info("Checking %d files", len(self.checkers)) - def report(self) -> Tuple[int, int]: + def report(self) -> tuple[int, int]: """Report all of the errors found in the managed file checkers. This iterates over each of the checkers and reports the errors sorted @@ -195,8 +196,8 @@ class Manager: def run_parallel(self) -> None: """Run the checkers in parallel.""" # fmt: off - final_results: Dict[str, List[Tuple[str, int, int, str, Optional[str]]]] = collections.defaultdict(list) # noqa: E501 - final_statistics: Dict[str, Dict[str, int]] = collections.defaultdict(dict) # noqa: E501 + final_results: dict[str, list[tuple[str, int, int, str, str | None]]] = collections.defaultdict(list) # noqa: E501 + final_statistics: dict[str, dict[str, int]] = collections.defaultdict(dict) # noqa: E501 # fmt: on pool = _try_initialize_processpool(self.jobs) @@ -254,7 +255,7 @@ class Manager: LOG.warning("Flake8 was interrupted by the user") raise exceptions.EarlyQuit("Early quit while running checks") - def start(self, paths: Optional[List[str]] = None) -> None: + def start(self, paths: list[str] | None = None) -> None: """Start checking files. :param paths: @@ -301,7 +302,7 @@ class FileChecker: """Provide helpful debugging representation.""" return f"FileChecker for {self.filename}" - def _make_processor(self) -> Optional[processor.FileProcessor]: + def _make_processor(self) -> processor.FileProcessor | None: try: return processor.FileProcessor(self.filename, self.options) except OSError as e: @@ -316,7 +317,7 @@ class FileChecker: def report( self, - error_code: Optional[str], + error_code: str | None, line_number: int, column: int, text: str, @@ -361,7 +362,7 @@ class FileChecker: ) @staticmethod - def _extract_syntax_information(exception: Exception) -> Tuple[int, int]: + def _extract_syntax_information(exception: Exception) -> tuple[int, int]: if ( len(exception.args) > 1 and exception.args[1] @@ -524,7 +525,7 @@ class FileChecker: self.run_physical_checks(file_processor.lines[-1]) self.run_logical_checks() - def run_checks(self) -> Tuple[str, Results, Dict[str, int]]: + def run_checks(self) -> tuple[str, Results, dict[str, int]]: """Run checks against the file.""" assert self.processor is not None try: @@ -592,7 +593,7 @@ def _pool_init() -> None: def _try_initialize_processpool( job_count: int, -) -> Optional[multiprocessing.pool.Pool]: +) -> multiprocessing.pool.Pool | None: """Return a new process pool instance if we are able to create one.""" try: return multiprocessing.Pool(job_count, _pool_init) @@ -617,13 +618,13 @@ def calculate_pool_chunksize(num_checkers: int, num_jobs: int) -> int: return max(num_checkers // (num_jobs * 2), 1) -def _run_checks(checker: FileChecker) -> Tuple[str, Results, Dict[str, int]]: +def _run_checks(checker: FileChecker) -> tuple[str, Results, dict[str, int]]: return checker.run_checks() def find_offset( offset: int, mapping: processor._LogicalMapping -) -> Tuple[int, int]: +) -> tuple[int, int]: """Find the offset tuple for a single offset.""" if isinstance(offset, tuple): return offset diff --git a/src/flake8/defaults.py b/src/flake8/defaults.py index a1c04fc..4ba0048 100644 --- a/src/flake8/defaults.py +++ b/src/flake8/defaults.py @@ -1,4 +1,6 @@ """Constants that define defaults.""" +from __future__ import annotations + import re EXCLUDE = ( diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py index 8c21064..b8592c8 100644 --- a/src/flake8/discover_files.py +++ b/src/flake8/discover_files.py @@ -1,4 +1,6 @@ """Functions related to discovering paths.""" +from __future__ import annotations + import logging import os.path from typing import Callable diff --git a/src/flake8/exceptions.py b/src/flake8/exceptions.py index 8e13cd8..18646e7 100644 --- a/src/flake8/exceptions.py +++ b/src/flake8/exceptions.py @@ -1,4 +1,5 @@ """Exception classes for all of Flake8.""" +from __future__ import annotations class Flake8Exception(Exception): diff --git a/src/flake8/formatting/__init__.py b/src/flake8/formatting/__init__.py index bf44801..732d0b6 100644 --- a/src/flake8/formatting/__init__.py +++ b/src/flake8/formatting/__init__.py @@ -1 +1,2 @@ """Submodule containing the default formatters for Flake8.""" +from __future__ import annotations diff --git a/src/flake8/formatting/_windows_color.py b/src/flake8/formatting/_windows_color.py index 1d2c73f..a06fdb9 100644 --- a/src/flake8/formatting/_windows_color.py +++ b/src/flake8/formatting/_windows_color.py @@ -2,6 +2,8 @@ See: https://github.com/pre-commit/pre-commit/blob/cb40e96/pre_commit/color.py """ +from __future__ import annotations + import sys if sys.platform == "win32": # pragma: no cover (windows) diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index 78d10e9..d986d65 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -1,11 +1,10 @@ """The base class and interface for all formatting plugins.""" +from __future__ import annotations + import argparse import os import sys from typing import IO -from typing import List -from typing import Optional -from typing import Tuple from flake8.formatting import _windows_color from flake8.statistics import Statistics @@ -46,7 +45,7 @@ class BaseFormatter: """ self.options = options self.filename = options.output_file - self.output_fd: Optional[IO[str]] = None + self.output_fd: IO[str] | None = None self.newline = "\n" self.color = options.color == "always" or ( options.color == "auto" @@ -84,7 +83,7 @@ class BaseFormatter: os.makedirs(dirname, exist_ok=True) self.output_fd = open(self.filename, "a") - def handle(self, error: "Violation") -> None: + def handle(self, error: Violation) -> None: """Handle an error reported by Flake8. This defaults to calling :meth:`format`, :meth:`show_source`, and @@ -99,7 +98,7 @@ class BaseFormatter: source = self.show_source(error) self.write(line, source) - def format(self, error: "Violation") -> Optional[str]: + def format(self, error: Violation) -> str | None: """Format an error reported by Flake8. This method **must** be implemented by subclasses. @@ -114,7 +113,7 @@ class BaseFormatter: "Subclass of BaseFormatter did not implement" " format." ) - def show_statistics(self, statistics: "Statistics") -> None: + def show_statistics(self, statistics: Statistics) -> None: """Format and print the statistics.""" for error_code in statistics.error_codes(): stats_for_error_code = statistics.statistics_for(error_code) @@ -123,7 +122,7 @@ class BaseFormatter: count += sum(stat.count for stat in stats_for_error_code) self._write(f"{count:<5} {error_code} {statistic.message}") - def show_benchmarks(self, benchmarks: List[Tuple[str, float]]) -> None: + def show_benchmarks(self, benchmarks: 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: @@ -144,7 +143,7 @@ class BaseFormatter: benchmark = float_format(statistic=statistic, value=value) self._write(benchmark) - def show_source(self, error: "Violation") -> Optional[str]: + def show_source(self, error: Violation) -> str | None: """Show the physical line generating the error. This also adds an indicator for the particular part of the line that @@ -178,7 +177,7 @@ class BaseFormatter: if self.output_fd is None or self.options.tee: sys.stdout.buffer.write(output.encode() + self.newline.encode()) - def write(self, line: Optional[str], source: Optional[str]) -> None: + def write(self, line: str | None, source: str | None) -> None: """Write the line either to the output file or stdout. This handles deciding whether to write to a file or print to standard diff --git a/src/flake8/formatting/default.py b/src/flake8/formatting/default.py index f43dc42..b5d08ff 100644 --- a/src/flake8/formatting/default.py +++ b/src/flake8/formatting/default.py @@ -1,6 +1,5 @@ """Default formatting class for Flake8.""" -from typing import Optional -from typing import Set +from __future__ import annotations from flake8.formatting import base from flake8.violation import Violation @@ -38,7 +37,7 @@ class SimpleFormatter(base.BaseFormatter): error_format: str - def format(self, error: "Violation") -> Optional[str]: + def format(self, error: Violation) -> str | None: """Format and write error out. If an output filename is specified, write formatted errors to that @@ -86,12 +85,12 @@ class FilenameOnly(SimpleFormatter): def after_init(self) -> None: """Initialize our set of filenames.""" - self.filenames_already_printed: Set[str] = set() + self.filenames_already_printed: set[str] = set() - def show_source(self, error: "Violation") -> Optional[str]: + def show_source(self, error: Violation) -> str | None: """Do not include the source code.""" - def format(self, error: "Violation") -> Optional[str]: + def format(self, error: Violation) -> str | None: """Ensure we only print each error once.""" if error.filename not in self.filenames_already_printed: self.filenames_already_printed.add(error.filename) @@ -103,8 +102,8 @@ class FilenameOnly(SimpleFormatter): class Nothing(base.BaseFormatter): """Print absolutely nothing.""" - def format(self, error: "Violation") -> Optional[str]: + def format(self, error: Violation) -> str | None: """Do nothing.""" - def show_source(self, error: "Violation") -> Optional[str]: + def show_source(self, error: Violation) -> str | None: """Do not print the source.""" diff --git a/src/flake8/main/__init__.py b/src/flake8/main/__init__.py index d3aa1de..85bcff4 100644 --- a/src/flake8/main/__init__.py +++ b/src/flake8/main/__init__.py @@ -1 +1,2 @@ """Module containing the logic for the Flake8 entry-points.""" +from __future__ import annotations diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 15c2477..13ece4e 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -1,15 +1,12 @@ """Module containing the application logic for Flake8.""" +from __future__ import annotations + import argparse import configparser import json import logging import time -from typing import Dict -from typing import List -from typing import Optional from typing import Sequence -from typing import Set -from typing import Tuple import flake8 from flake8 import checker @@ -38,27 +35,27 @@ class Application: #: The timestamp when the Application instance was instantiated. self.start_time = time.time() #: The timestamp when the Application finished reported errors. - self.end_time: Optional[float] = None + self.end_time: float | None = None #: The prelimary argument parser for handling options required for #: obtaining and parsing the configuration file. self.prelim_arg_parser = options.stage1_arg_parser() #: The instance of :class:`flake8.options.manager.OptionManager` used #: to parse and handle the options and arguments passed by the user - self.option_manager: Optional[manager.OptionManager] = None + self.option_manager: manager.OptionManager | None = None - self.plugins: Optional[finder.Plugins] = None + self.plugins: finder.Plugins | None = None #: The user-selected formatter from :attr:`formatting_plugins` - self.formatter: Optional[BaseFormatter] = None + self.formatter: BaseFormatter | None = None #: The :class:`flake8.style_guide.StyleGuideManager` built from the #: user's options - self.guide: Optional[style_guide.StyleGuideManager] = None + self.guide: style_guide.StyleGuideManager | None = None #: The :class:`flake8.checker.Manager` that will handle running all of #: the checks selected by the user. - self.file_checker_manager: Optional[checker.Manager] = None + self.file_checker_manager: checker.Manager | None = None #: The user-supplied options parsed into an instance of #: :class:`argparse.Namespace` - self.options: Optional[argparse.Namespace] = None + self.options: argparse.Namespace | None = None #: The number of errors, warnings, and other messages after running #: flake8 and taking into account ignored errors and lines. self.result_count = 0 @@ -70,11 +67,11 @@ class Application: self.catastrophic_failure = False #: The parsed diff information - self.parsed_diff: Dict[str, Set[int]] = {} + self.parsed_diff: dict[str, set[int]] = {} def parse_preliminary_options( self, argv: Sequence[str] - ) -> Tuple[argparse.Namespace, List[str]]: + ) -> tuple[argparse.Namespace, list[str]]: """Get preliminary options from the CLI, pre-plugin-loading. We need to know the values of a few standard options so that we can @@ -111,8 +108,8 @@ class Application: cfg: configparser.RawConfigParser, cfg_dir: str, *, - enable_extensions: Optional[str], - require_plugins: Optional[str], + enable_extensions: str | None, + require_plugins: str | None, ) -> None: """Find and load the plugins for this application. @@ -143,7 +140,7 @@ class Application: self, cfg: configparser.RawConfigParser, cfg_dir: str, - argv: List[str], + argv: list[str], ) -> None: """Parse configuration files and the CLI options.""" assert self.option_manager is not None @@ -218,7 +215,7 @@ class Application: assert self.options is not None assert self.file_checker_manager is not None if self.options.diff: - files: Optional[List[str]] = sorted(self.parsed_diff) + files: list[str] | None = sorted(self.parsed_diff) if not files: return else: diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py index b4bb202..01a67ac 100644 --- a/src/flake8/main/cli.py +++ b/src/flake8/main/cli.py @@ -1,12 +1,13 @@ """Command-line implementation of flake8.""" +from __future__ import annotations + import sys -from typing import Optional from typing import Sequence from flake8.main import application -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: """Execute the main bit of the application. This handles the creation of an instance of :class:`Application`, runs it, diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index 03671bc..c3a8b0b 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -1,12 +1,13 @@ """Module containing the logic for our debugging logic.""" +from __future__ import annotations + import platform from typing import Any -from typing import Dict from flake8.plugins.finder import Plugins -def information(version: str, plugins: Plugins) -> Dict[str, Any]: +def information(version: str, plugins: Plugins) -> dict[str, Any]: """Generate the information to be printed for the bug report.""" versions = sorted( { diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 4c9dfb8..d603232 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -1,4 +1,6 @@ """Contains the logic for all of the default options for Flake8.""" +from __future__ import annotations + import argparse from flake8 import defaults diff --git a/src/flake8/options/__init__.py b/src/flake8/options/__init__.py index cc20daa..3578223 100644 --- a/src/flake8/options/__init__.py +++ b/src/flake8/options/__init__.py @@ -10,3 +10,4 @@ to aggregate configuration into one object used by plugins and Flake8. """ +from __future__ import annotations diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index 580def6..af8e744 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -3,10 +3,11 @@ This holds the logic that uses the collected and merged config files and applies the user-specified command-line configuration on top of it. """ +from __future__ import annotations + import argparse import configparser import logging -from typing import Optional from typing import Sequence from flake8.options import config @@ -19,7 +20,7 @@ def aggregate_options( manager: OptionManager, cfg: configparser.RawConfigParser, cfg_dir: str, - argv: Optional[Sequence[str]], + argv: Sequence[str] | None, ) -> argparse.Namespace: """Aggregate and merge CLI and config file options.""" # Get defaults from the option parser diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index daf8529..e158737 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -1,12 +1,10 @@ """Config handling logic for Flake8.""" +from __future__ import annotations + import configparser import logging import os.path from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple from flake8 import exceptions from flake8.options.manager import OptionManager @@ -14,13 +12,13 @@ from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) -def _stat_key(s: str) -> Tuple[int, int]: +def _stat_key(s: str) -> tuple[int, int]: # same as what's used by samefile / samestat st = os.stat(s) return st.st_ino, st.st_dev -def _find_config_file(path: str) -> Optional[str]: +def _find_config_file(path: str) -> str | None: # on windows if the homedir isn't detected this returns back `~` home = os.path.expanduser("~") try: @@ -55,11 +53,11 @@ def _find_config_file(path: str) -> Optional[str]: def load_config( - config: Optional[str], - extra: List[str], + config: str | None, + extra: list[str], *, isolated: bool = False, -) -> Tuple[configparser.RawConfigParser, str]: +) -> tuple[configparser.RawConfigParser, str]: """Load the configuration given the user options. - in ``isolated`` mode, return an empty configuration @@ -97,7 +95,7 @@ def parse_config( option_manager: OptionManager, cfg: configparser.RawConfigParser, cfg_dir: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Parse and normalize the typed configuration options.""" if "flake8" not in cfg: return {} diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index ff5a229..e333c9e 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -1,18 +1,14 @@ """Option handling and Option management logic.""" +from __future__ import annotations + import argparse import enum import functools import logging from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Mapping -from typing import Optional from typing import Sequence -from typing import Tuple -from typing import Type -from typing import Union from flake8 import utils from flake8.plugins.finder import Plugins @@ -24,7 +20,7 @@ LOG = logging.getLogger(__name__) _ARG = enum.Enum("_ARG", "NO") -_optparse_callable_map: Dict[str, Union[Type[Any], _ARG]] = { +_optparse_callable_map: dict[str, type[Any] | _ARG] = { "int": int, "long": int, "string": str, @@ -44,7 +40,7 @@ class _CallbackAction(argparse.Action): *args: Any, callback: Callable[..., Any], callback_args: Sequence[Any] = (), - callback_kwargs: Optional[Dict[str, Any]] = None, + callback_kwargs: dict[str, Any] | None = None, **kwargs: Any, ) -> None: self._callback = callback @@ -56,8 +52,8 @@ class _CallbackAction(argparse.Action): self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Optional[Union[Sequence[str], str]], - option_string: Optional[str] = None, + values: Sequence[str] | str | None, + option_string: str | None = None, ) -> None: if not values: values = None @@ -78,8 +74,8 @@ def _flake8_normalize( *args: str, comma_separated_list: bool = False, normalize_paths: bool = False, -) -> Union[str, List[str]]: - ret: Union[str, List[str]] = value +) -> str | list[str]: + ret: str | list[str] = value if comma_separated_list and isinstance(ret, str): ret = utils.parse_comma_separated_list(value) @@ -97,24 +93,24 @@ class Option: def __init__( self, - short_option_name: Union[str, _ARG] = _ARG.NO, - long_option_name: Union[str, _ARG] = _ARG.NO, + short_option_name: str | _ARG = _ARG.NO, + long_option_name: str | _ARG = _ARG.NO, # Options below here are taken from the optparse.Option class - action: Union[str, Type[argparse.Action], _ARG] = _ARG.NO, - default: Union[Any, _ARG] = _ARG.NO, - type: Union[str, Callable[..., Any], _ARG] = _ARG.NO, - dest: Union[str, _ARG] = _ARG.NO, - nargs: Union[int, str, _ARG] = _ARG.NO, - const: Union[Any, _ARG] = _ARG.NO, - choices: Union[Sequence[Any], _ARG] = _ARG.NO, - help: Union[str, _ARG] = _ARG.NO, - metavar: Union[str, _ARG] = _ARG.NO, + action: str | type[argparse.Action] | _ARG = _ARG.NO, + default: Any | _ARG = _ARG.NO, + type: str | Callable[..., Any] | _ARG = _ARG.NO, + dest: str | _ARG = _ARG.NO, + nargs: int | str | _ARG = _ARG.NO, + const: Any | _ARG = _ARG.NO, + choices: Sequence[Any] | _ARG = _ARG.NO, + help: str | _ARG = _ARG.NO, + metavar: str | _ARG = _ARG.NO, # deprecated optparse-only options - callback: Union[Callable[..., Any], _ARG] = _ARG.NO, - callback_args: Union[Sequence[Any], _ARG] = _ARG.NO, - callback_kwargs: Union[Mapping[str, Any], _ARG] = _ARG.NO, + callback: Callable[..., Any] | _ARG = _ARG.NO, + callback_args: Sequence[Any] | _ARG = _ARG.NO, + callback_kwargs: Mapping[str, Any] | _ARG = _ARG.NO, # Options below are taken from argparse.ArgumentParser.add_argument - required: Union[bool, _ARG] = _ARG.NO, + required: bool | _ARG = _ARG.NO, # Options below here are specific to Flake8 parse_from_config: bool = False, comma_separated_list: bool = False, @@ -247,7 +243,7 @@ class Option: self.help = help self.metavar = metavar self.required = required - self.option_kwargs: Dict[str, Union[Any, _ARG]] = { + self.option_kwargs: dict[str, Any | _ARG] = { "action": self.action, "default": self.default, "type": self.type, @@ -268,7 +264,7 @@ class Option: self.comma_separated_list = comma_separated_list self.normalize_paths = normalize_paths - self.config_name: Optional[str] = None + self.config_name: str | None = None if parse_from_config: if long_option_name is _ARG.NO: raise ValueError( @@ -280,7 +276,7 @@ class Option: self._opt = None @property - def filtered_option_kwargs(self) -> Dict[str, Any]: + def filtered_option_kwargs(self) -> dict[str, Any]: """Return any actually-specified arguments.""" return { k: v for k, v in self.option_kwargs.items() if v is not _ARG.NO @@ -307,7 +303,7 @@ class Option: return value - def to_argparse(self) -> Tuple[List[str], Dict[str, Any]]: + def to_argparse(self) -> tuple[list[str], dict[str, Any]]: """Convert a Flake8 Option to argparse ``add_argument`` arguments.""" return self.option_args, self.filtered_option_kwargs @@ -320,7 +316,7 @@ class OptionManager: *, version: str, plugin_versions: str, - parents: List[argparse.ArgumentParser], + parents: list[argparse.ArgumentParser], ) -> None: """Initialize an instance of an OptionManager. @@ -350,17 +346,17 @@ class OptionManager: ) self.parser.add_argument("filenames", nargs="*", metavar="filename") - self.config_options_dict: Dict[str, Option] = {} - self.options: List[Option] = [] - self.extended_default_ignore: List[str] = [] - self.extended_default_select: List[str] = [] + self.config_options_dict: dict[str, Option] = {} + self.options: list[Option] = [] + self.extended_default_ignore: list[str] = [] + self.extended_default_select: list[str] = [] - self._current_group: Optional[argparse._ArgumentGroup] = None + self._current_group: argparse._ArgumentGroup | None = None # TODO: maybe make this a free function to reduce api surface area def register_plugins(self, plugins: Plugins) -> None: """Register the plugin options (if needed).""" - groups: Dict[str, argparse._ArgumentGroup] = {} + groups: dict[str, argparse._ArgumentGroup] = {} def _set_group(name: str) -> None: try: @@ -428,8 +424,8 @@ class OptionManager: def parse_args( self, - args: Optional[Sequence[str]] = None, - values: Optional[argparse.Namespace] = None, + args: Sequence[str] | None = None, + values: argparse.Namespace | None = None, ) -> argparse.Namespace: """Proxy to calling the OptionParser's parse_args method.""" if values: diff --git a/src/flake8/plugins/__init__.py b/src/flake8/plugins/__init__.py index fda6a44..b540313 100644 --- a/src/flake8/plugins/__init__.py +++ b/src/flake8/plugins/__init__.py @@ -1 +1,2 @@ """Submodule of built-in plugins and plugin managers.""" +from __future__ import annotations diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 9e9e3af..c051488 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -1,4 +1,6 @@ """Functions related to finding and loading plugins.""" +from __future__ import annotations + import configparser import inspect import itertools @@ -6,14 +8,9 @@ import logging import re import sys from typing import Any -from typing import Dict -from typing import FrozenSet from typing import Generator from typing import Iterable -from typing import List from typing import NamedTuple -from typing import Optional -from typing import Tuple from flake8 import utils from flake8._compat import importlib_metadata @@ -45,7 +42,7 @@ class LoadedPlugin(NamedTuple): plugin: Plugin obj: Any - parameters: Dict[str, bool] + parameters: dict[str, bool] @property def entry_name(self) -> str: @@ -61,17 +58,17 @@ class LoadedPlugin(NamedTuple): class Checkers(NamedTuple): """Classified plugins needed for checking.""" - tree: List[LoadedPlugin] - logical_line: List[LoadedPlugin] - physical_line: List[LoadedPlugin] + tree: list[LoadedPlugin] + logical_line: list[LoadedPlugin] + physical_line: list[LoadedPlugin] class Plugins(NamedTuple): """Classified plugins.""" checkers: Checkers - reporters: Dict[str, LoadedPlugin] - disabled: List[LoadedPlugin] + reporters: dict[str, LoadedPlugin] + disabled: list[LoadedPlugin] def all_plugins(self) -> Generator[LoadedPlugin, None, None]: """Return an iterator over all :class:`LoadedPlugin`s.""" @@ -96,12 +93,12 @@ class Plugins(NamedTuple): class PluginOptions(NamedTuple): """Options related to plugin loading.""" - local_plugin_paths: Tuple[str, ...] - enable_extensions: FrozenSet[str] - require_plugins: FrozenSet[str] + local_plugin_paths: tuple[str, ...] + enable_extensions: frozenset[str] + require_plugins: frozenset[str] @classmethod - def blank(cls) -> "PluginOptions": + def blank(cls) -> PluginOptions: """Make a blank PluginOptions, mostly used for tests.""" return cls( local_plugin_paths=(), @@ -113,8 +110,8 @@ class PluginOptions(NamedTuple): def _parse_option( cfg: configparser.RawConfigParser, cfg_opt_name: str, - opt: Optional[str], -) -> List[str]: + opt: str | None, +) -> list[str]: # specified on commandline: use that if opt is not None: return utils.parse_comma_separated_list(opt) @@ -133,8 +130,8 @@ def parse_plugin_options( cfg: configparser.RawConfigParser, cfg_dir: str, *, - enable_extensions: Optional[str], - require_plugins: Optional[str], + enable_extensions: str | None, + require_plugins: str | None, ) -> PluginOptions: """Parse plugin loading related options.""" paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() @@ -231,8 +228,8 @@ def _find_local_plugins( def _check_required_plugins( - plugins: List[Plugin], - expected: FrozenSet[str], + plugins: list[Plugin], + expected: frozenset[str], ) -> None: plugin_names = { utils.normalize_pypi_name(plugin.package) for plugin in plugins @@ -252,7 +249,7 @@ def _check_required_plugins( def find_plugins( cfg: configparser.RawConfigParser, opts: PluginOptions, -) -> List[Plugin]: +) -> list[Plugin]: """Discovers all plugins (but does not load them).""" ret = [*_find_importlib_plugins(), *_find_local_plugins(cfg)] @@ -264,7 +261,7 @@ def find_plugins( return ret -def _parameters_for(func: Any) -> Dict[str, bool]: +def _parameters_for(func: Any) -> dict[str, bool]: """Return the parameters for the plugin. This will inspect the plugin and return either the function parameters @@ -305,15 +302,15 @@ def _load_plugin(plugin: Plugin) -> LoadedPlugin: def _import_plugins( - plugins: List[Plugin], + plugins: list[Plugin], opts: PluginOptions, -) -> List[LoadedPlugin]: +) -> list[LoadedPlugin]: sys.path.extend(opts.local_plugin_paths) return [_load_plugin(p) for p in plugins] def _classify_plugins( - plugins: List[LoadedPlugin], + plugins: list[LoadedPlugin], opts: PluginOptions, ) -> Plugins: tree = [] @@ -358,7 +355,7 @@ def _classify_plugins( def load_plugins( - plugins: List[Plugin], + plugins: list[Plugin], opts: PluginOptions, ) -> Plugins: """Load and classify all flake8 plugins. diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py index 753af23..4b0d67f 100644 --- a/src/flake8/plugins/pycodestyle.py +++ b/src/flake8/plugins/pycodestyle.py @@ -1,8 +1,9 @@ """Generated using ./bin/gen-pycodestyle-plugin.""" # fmt: off +from __future__ import annotations + from typing import Any from typing import Generator -from typing import Tuple from pycodestyle import ambiguous_identifier as _ambiguous_identifier from pycodestyle import bare_except as _bare_except @@ -60,7 +61,7 @@ def pycodestyle_logical( previous_unindented_logical_line: Any, tokens: Any, verbose: Any, -) -> Generator[Tuple[int, str], None, None]: +) -> Generator[tuple[int, str], None, None]: """Run pycodestyle logical checks.""" yield from _ambiguous_identifier(logical_line, tokens) yield from _bare_except(logical_line, noqa) @@ -104,7 +105,7 @@ def pycodestyle_physical( noqa: Any, physical_line: Any, total_lines: Any, -) -> Generator[Tuple[int, str], None, None]: +) -> Generator[tuple[int, str], None, None]: """Run pycodestyle physical checks.""" ret = _maximum_line_length(physical_line, max_line_length, multiline, line_number, noqa) # noqa: E501 if ret is not None: diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 54eaeca..7b99fd4 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -1,13 +1,12 @@ """Plugin built-in to Flake8 to treat pyflakes as a plugin.""" +from __future__ import annotations + import argparse import ast import os import tokenize from typing import Any from typing import Generator -from typing import List -from typing import Tuple -from typing import Type import pyflakes.checker @@ -68,13 +67,13 @@ class FlakesChecker(pyflakes.checker.Checker): """Subclass the Pyflakes checker to conform with the flake8 API.""" with_doctest = False - include_in_doctest: List[str] = [] - exclude_from_doctest: List[str] = [] + include_in_doctest: list[str] = [] + exclude_from_doctest: list[str] = [] def __init__( self, tree: ast.AST, - file_tokens: List[tokenize.TokenInfo], + file_tokens: list[tokenize.TokenInfo], filename: str, ) -> None: """Initialize the PyFlakes plugin with an AST tree and filename.""" @@ -180,7 +179,7 @@ class FlakesChecker(pyflakes.checker.Checker): f"both for doctesting." ) - def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]: + def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]: """Run the plugin.""" for message in self.messages: col = getattr(message, "col", 0) diff --git a/src/flake8/plugins/reporter.py b/src/flake8/plugins/reporter.py index 5bbbd81..f63b20c 100644 --- a/src/flake8/plugins/reporter.py +++ b/src/flake8/plugins/reporter.py @@ -1,7 +1,8 @@ """Functions for construcing the requested report plugin.""" +from __future__ import annotations + import argparse import logging -from typing import Dict from flake8.formatting.base import BaseFormatter from flake8.plugins.finder import LoadedPlugin @@ -10,7 +11,7 @@ LOG = logging.getLogger(__name__) def make( - reporters: Dict[str, LoadedPlugin], + reporters: dict[str, LoadedPlugin], options: argparse.Namespace, ) -> BaseFormatter: """Make the formatter from the requested user options. diff --git a/src/flake8/processor.py b/src/flake8/processor.py index fa9bd2f..644192d 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -1,14 +1,14 @@ """Module containing our file processor that tokenizes a file for checks.""" +from __future__ import annotations + import argparse import ast import contextlib import logging import tokenize from typing import Any -from typing import Dict from typing import Generator from typing import List -from typing import Optional from typing import Tuple from flake8 import defaults @@ -61,7 +61,7 @@ class FileProcessor: self, filename: str, options: argparse.Namespace, - lines: Optional[List[str]] = None, + lines: list[str] | None = None, ) -> None: """Initialice our file processor. @@ -78,13 +78,13 @@ class FileProcessor: #: Number of blank lines self.blank_lines = 0 #: Checker states for each plugin? - self._checker_states: Dict[str, Dict[Any, Any]] = {} + self._checker_states: dict[str, dict[Any, Any]] = {} #: Current checker state - self.checker_state: Dict[Any, Any] = {} + self.checker_state: dict[Any, Any] = {} #: User provided option for hang closing self.hang_closing = options.hang_closing #: Character used for indentation - self.indent_char: Optional[str] = None + self.indent_char: str | None = None #: Current level of indentation self.indent_level = 0 #: Number of spaces used for indentation @@ -106,19 +106,19 @@ class FileProcessor: #: Previous unindented (i.e. top-level) logical line self.previous_unindented_logical_line = "" #: Current set of tokens - self.tokens: List[tokenize.TokenInfo] = [] + self.tokens: list[tokenize.TokenInfo] = [] #: Total number of lines in the file self.total_lines = len(self.lines) #: Verbosity level of Flake8 self.verbose = options.verbose #: Statistics dictionary self.statistics = {"logical lines": 0} - self._file_tokens: Optional[List[tokenize.TokenInfo]] = None + self._file_tokens: list[tokenize.TokenInfo] | None = None # map from line number to the line we'll search for `noqa` in - self._noqa_line_mapping: Optional[Dict[int, str]] = None + self._noqa_line_mapping: dict[int, str] | None = None @property - def file_tokens(self) -> List[tokenize.TokenInfo]: + def file_tokens(self) -> list[tokenize.TokenInfo]: """Return the complete set of tokens for a file.""" if self._file_tokens is None: line_iter = iter(self.lines) @@ -217,7 +217,7 @@ class FileProcessor: """Build an abstract syntax tree from the list of lines.""" return ast.parse("".join(self.lines)) - def build_logical_line(self) -> Tuple[str, str, _LogicalMapping]: + def build_logical_line(self) -> tuple[str, str, _LogicalMapping]: """Build a logical line from the current tokens list.""" comments, logical, mapping_list = self.build_logical_line_tokens() joined_comments = "".join(comments) @@ -240,9 +240,9 @@ class FileProcessor: def keyword_arguments_for( self, - parameters: Dict[str, bool], - arguments: Dict[str, Any], - ) -> Dict[str, Any]: + parameters: dict[str, bool], + arguments: dict[str, Any], + ) -> dict[str, Any]: """Generate the keyword arguments for a list of parameters.""" ret = {} for param, required in parameters.items(): @@ -269,12 +269,12 @@ class FileProcessor: self.tokens.append(token) yield token - def _noqa_line_range(self, min_line: int, max_line: int) -> Dict[int, str]: + def _noqa_line_range(self, min_line: int, max_line: int) -> dict[int, str]: line_range = range(min_line, max_line + 1) joined = "".join(self.lines[min_line - 1 : max_line]) return dict.fromkeys(line_range, joined) - def noqa_line_for(self, line_number: int) -> Optional[str]: + def noqa_line_for(self, line_number: int) -> str | None: """Retrieve the line which will be used to determine noqa.""" if self._noqa_line_mapping is None: try: @@ -324,7 +324,7 @@ class FileProcessor: self.indent_char = line[0] return line - def read_lines(self) -> List[str]: + def read_lines(self) -> list[str]: """Read the lines for this file checker.""" if self.filename is None or self.filename == "-": self.filename = self.options.stdin_display_name or "stdin" @@ -333,7 +333,7 @@ class FileProcessor: lines = self.read_lines_from_filename() return lines - def read_lines_from_filename(self) -> List[str]: + def read_lines_from_filename(self) -> list[str]: """Read the lines for a file.""" try: with tokenize.open(self.filename) as fd: @@ -344,7 +344,7 @@ class FileProcessor: with open(self.filename, encoding="latin-1") as fd: return fd.readlines() - def read_lines_from_stdin(self) -> List[str]: + def read_lines_from_stdin(self) -> list[str]: """Read the lines from standard in.""" return utils.stdin_get_lines() diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index ae89be1..a33e6a6 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -1,9 +1,8 @@ """Statistic collection logic for Flake8.""" -from typing import Dict +from __future__ import annotations + from typing import Generator -from typing import List from typing import NamedTuple -from typing import Optional from flake8.violation import Violation @@ -13,9 +12,9 @@ class Statistics: def __init__(self) -> None: """Initialize the underlying dictionary for our statistics.""" - self._store: Dict[Key, "Statistic"] = {} + self._store: dict[Key, Statistic] = {} - def error_codes(self) -> List[str]: + def error_codes(self) -> list[str]: """Return all unique error codes stored. :returns: @@ -23,7 +22,7 @@ class Statistics: """ return sorted({key.code for key in self._store}) - def record(self, error: "Violation") -> None: + def record(self, error: Violation) -> None: """Add the fact that the error was seen in the file. :param error: @@ -36,8 +35,8 @@ class Statistics: self._store[key].increment() def statistics_for( - self, prefix: str, filename: Optional[str] = None - ) -> Generator["Statistic", None, None]: + self, prefix: str, filename: str | None = None + ) -> Generator[Statistic, None, None]: """Generate statistics for the prefix and filename. If you have a :class:`Statistics` object that has recorded errors, @@ -79,11 +78,11 @@ class Key(NamedTuple): code: str @classmethod - def create_from(cls, error: "Violation") -> "Key": + def create_from(cls, error: Violation) -> Key: """Create a Key from :class:`flake8.violation.Violation`.""" return cls(filename=error.filename, code=error.code) - def matches(self, prefix: str, filename: Optional[str]) -> bool: + def matches(self, prefix: str, filename: str | None) -> bool: """Determine if this key matches some constraints. :param prefix: @@ -118,7 +117,7 @@ class Statistic: self.count = count @classmethod - def create_from(cls, error: "Violation") -> "Statistic": + def create_from(cls, error: Violation) -> Statistic: """Create a Statistic from a :class:`flake8.violation.Violation`.""" return cls( error_code=error.code, diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index b7115a0..2fee0f3 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -1,4 +1,6 @@ """Implementation of the StyleGuide used by Flake8.""" +from __future__ import annotations + import argparse import contextlib import copy @@ -6,14 +8,8 @@ import enum import functools import itertools import logging -from typing import Dict from typing import Generator -from typing import List -from typing import Optional from typing import Sequence -from typing import Set -from typing import Tuple -from typing import Union from flake8 import defaults from flake8 import statistics @@ -49,20 +45,20 @@ class Decision(enum.Enum): def _explicitly_chosen( *, - option: Optional[List[str]], - extend: Optional[List[str]], -) -> Tuple[str, ...]: + option: list[str] | None, + extend: list[str] | None, +) -> tuple[str, ...]: ret = [*(option or []), *(extend or [])] return tuple(sorted(ret, reverse=True)) def _select_ignore( *, - option: Optional[List[str]], - default: Tuple[str, ...], - extended_default: List[str], - extend: Optional[List[str]], -) -> Tuple[str, ...]: + option: list[str] | None, + default: tuple[str, ...], + extended_default: list[str], + extend: list[str] | None, +) -> tuple[str, ...]: # option was explicitly set, ignore the default and extended default if option is not None: ret = [*option, *(extend or [])] @@ -80,7 +76,7 @@ class DecisionEngine: def __init__(self, options: argparse.Namespace) -> None: """Initialize the engine.""" - self.cache: Dict[str, Decision] = {} + self.cache: dict[str, Decision] = {} self.selected_explicitly = _explicitly_chosen( option=options.select, @@ -104,7 +100,7 @@ class DecisionEngine: extend=options.extend_ignore, ) - def was_selected(self, code: str) -> Union[Selected, Ignored]: + def was_selected(self, code: str) -> Selected | Ignored: """Determine if the code has been selected by the user. :param code: The code for the check that has been run. @@ -122,7 +118,7 @@ class DecisionEngine: else: return Ignored.Implicitly - def was_ignored(self, code: str) -> Union[Selected, Ignored]: + def was_ignored(self, code: str) -> Selected | Ignored: """Determine if the code has been ignored by the user. :param code: @@ -211,7 +207,7 @@ class StyleGuideManager: self, options: argparse.Namespace, formatter: base_formatter.BaseFormatter, - decider: Optional[DecisionEngine] = None, + decider: DecisionEngine | None = None, ) -> None: """Initialize our StyleGuide. @@ -221,7 +217,7 @@ class StyleGuideManager: self.formatter = formatter self.stats = statistics.Statistics() self.decider = decider or DecisionEngine(options) - self.style_guides: List[StyleGuide] = [] + self.style_guides: list[StyleGuide] = [] self.default_style_guide = StyleGuide( options, formatter, self.stats, decider=decider ) @@ -238,7 +234,7 @@ class StyleGuideManager: def populate_style_guides_with( self, options: argparse.Namespace - ) -> Generator["StyleGuide", None, None]: + ) -> Generator[StyleGuide, None, None]: """Generate style guides from the per-file-ignores option. :param options: @@ -252,7 +248,7 @@ class StyleGuideManager: filename=filename, extend_ignore_with=violations ) - def _style_guide_for(self, filename: str) -> "StyleGuide": + def _style_guide_for(self, filename: str) -> StyleGuide: """Find the StyleGuide for the filename in particular.""" return max( (g for g in self.style_guides if g.applies_to(filename)), @@ -262,7 +258,7 @@ class StyleGuideManager: @contextlib.contextmanager def processing_file( self, filename: str - ) -> Generator["StyleGuide", None, None]: + ) -> Generator[StyleGuide, None, None]: """Record the fact that we're processing the file's results.""" guide = self.style_guide_for(filename) with guide.processing_file(filename): @@ -275,7 +271,7 @@ class StyleGuideManager: line_number: int, column_number: int, text: str, - physical_line: Optional[str] = None, + physical_line: str | None = None, ) -> int: """Handle an error reported by a check. @@ -302,7 +298,7 @@ class StyleGuideManager: code, filename, line_number, column_number, text, physical_line ) - def add_diff_ranges(self, diffinfo: Dict[str, Set[int]]) -> None: + def add_diff_ranges(self, diffinfo: 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 @@ -323,8 +319,8 @@ class StyleGuide: options: argparse.Namespace, formatter: base_formatter.BaseFormatter, stats: statistics.Statistics, - filename: Optional[str] = None, - decider: Optional[DecisionEngine] = None, + filename: str | None = None, + decider: DecisionEngine | None = None, ): """Initialize our StyleGuide. @@ -337,7 +333,7 @@ class StyleGuide: self.filename = filename if self.filename: self.filename = utils.normalize_path(self.filename) - self._parsed_diff: Dict[str, Set[int]] = {} + self._parsed_diff: dict[str, set[int]] = {} def __repr__(self) -> str: """Make it easier to debug which StyleGuide we're using.""" @@ -345,9 +341,9 @@ class StyleGuide: def copy( self, - filename: Optional[str] = None, - extend_ignore_with: Optional[Sequence[str]] = None, - ) -> "StyleGuide": + filename: str | None = None, + extend_ignore_with: Sequence[str] | None = None, + ) -> StyleGuide: """Create a copy of this style guide with different values.""" filename = filename or self.filename options = copy.deepcopy(self.options) @@ -360,7 +356,7 @@ class StyleGuide: @contextlib.contextmanager def processing_file( self, filename: str - ) -> Generator["StyleGuide", None, None]: + ) -> Generator[StyleGuide, None, None]: """Record the fact that we're processing the file's results.""" self.formatter.beginning(filename) yield self @@ -405,7 +401,7 @@ class StyleGuide: line_number: int, column_number: int, text: str, - physical_line: Optional[str] = None, + physical_line: str | None = None, ) -> int: """Handle an error reported by a check. @@ -451,7 +447,7 @@ class StyleGuide: return 1 return 0 - def add_diff_ranges(self, diffinfo: Dict[str, Set[int]]) -> None: + def add_diff_ranges(self, diffinfo: 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 cc47ffc..60555a9 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -1,4 +1,6 @@ """Utility methods for flake8.""" +from __future__ import annotations + import collections import fnmatch as _fnmatch import functools @@ -10,15 +12,9 @@ import re import sys import textwrap import tokenize -from typing import Dict -from typing import List from typing import NamedTuple -from typing import Optional from typing import Pattern from typing import Sequence -from typing import Set -from typing import Tuple -from typing import Union from flake8 import exceptions @@ -30,7 +26,7 @@ NORMALIZE_PACKAGE_NAME_RE = re.compile(r"[-_.]+") def parse_comma_separated_list( value: str, regexp: Pattern[str] = COMMA_SEPARATED_LIST_RE -) -> List[str]: +) -> list[str]: """Parse a comma-separated list. :param value: @@ -64,7 +60,7 @@ _FILE_LIST_TOKEN_TYPES = [ ] -def _tokenize_files_to_codes_mapping(value: str) -> List[_Token]: +def _tokenize_files_to_codes_mapping(value: str) -> list[_Token]: tokens = [] i = 0 while i < len(value): @@ -82,8 +78,8 @@ def _tokenize_files_to_codes_mapping(value: str) -> List[_Token]: def parse_files_to_codes_mapping( # noqa: C901 - value_: Union[Sequence[str], str] -) -> List[Tuple[str, List[str]]]: + value_: Sequence[str] | str, +) -> list[tuple[str, list[str]]]: """Parse a files-to-codes mapping. A files-to-codes mapping a sequence of values specified as @@ -97,15 +93,15 @@ def parse_files_to_codes_mapping( # noqa: C901 else: value = value_ - ret: List[Tuple[str, List[str]]] = [] + ret: list[tuple[str, list[str]]] = [] if not value.strip(): return ret class State: seen_sep = True seen_colon = False - filenames: List[str] = [] - codes: List[str] = [] + filenames: list[str] = [] + codes: list[str] = [] def _reset() -> None: if State.codes: @@ -157,7 +153,7 @@ def parse_files_to_codes_mapping( # noqa: C901 def normalize_paths( paths: Sequence[str], parent: str = os.curdir -) -> List[str]: +) -> list[str]: """Normalize a list of paths relative to a parent directory. :returns: @@ -201,12 +197,12 @@ def stdin_get_value() -> str: return stdin_value.decode("utf-8") -def stdin_get_lines() -> List[str]: +def stdin_get_lines() -> list[str]: """Return lines of stdin split according to file splitting.""" return list(io.StringIO(stdin_get_value())) -def parse_unified_diff(diff: Optional[str] = None) -> Dict[str, Set[int]]: +def parse_unified_diff(diff: str | None = None) -> dict[str, set[int]]: """Parse the unified diff passed on stdin. :returns: @@ -218,7 +214,7 @@ def parse_unified_diff(diff: Optional[str] = None) -> Dict[str, Set[int]]: number_of_rows = None current_path = None - parsed_paths: Dict[str, Set[int]] = collections.defaultdict(set) + parsed_paths: dict[str, set[int]] = collections.defaultdict(set) for line in diff.splitlines(): if number_of_rows: if not line or line[0] != "-": @@ -271,7 +267,7 @@ def parse_unified_diff(diff: Optional[str] = None) -> Dict[str, Set[int]]: return parsed_paths -def is_using_stdin(paths: List[str]) -> bool: +def is_using_stdin(paths: list[str]) -> bool: """Determine if we're going to read from stdin. :param paths: diff --git a/src/flake8/violation.py b/src/flake8/violation.py index d2d2578..45834b2 100644 --- a/src/flake8/violation.py +++ b/src/flake8/violation.py @@ -1,12 +1,11 @@ """Contains the Violation error class used internally.""" +from __future__ import annotations + import functools import linecache import logging -from typing import Dict from typing import Match from typing import NamedTuple -from typing import Optional -from typing import Set from flake8 import defaults from flake8 import utils @@ -16,7 +15,7 @@ LOG = logging.getLogger(__name__) @functools.lru_cache(maxsize=512) -def _find_noqa(physical_line: str) -> Optional[Match[str]]: +def _find_noqa(physical_line: str) -> Match[str] | None: return defaults.NOQA_INLINE_REGEXP.search(physical_line) @@ -28,7 +27,7 @@ class Violation(NamedTuple): line_number: int column_number: int text: str - physical_line: Optional[str] + physical_line: str | None def is_inline_ignored(self, disable_noqa: bool) -> bool: """Determine if a comment has been added to ignore this line. @@ -69,7 +68,7 @@ class Violation(NamedTuple): ) return False - def is_in(self, diff: Dict[str, Set[int]]) -> bool: + def is_in(self, diff: dict[str, set[int]]) -> bool: """Determine if the violation is included in a diff's line ranges. This function relies on the parsed data added via diff --git a/tests/__init__.py b/tests/__init__.py index f7ac891..ee1f2a0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,2 @@ """This is here because mypy doesn't understand PEP 420.""" +from __future__ import annotations diff --git a/tests/conftest.py b/tests/conftest.py index 0f48309..ac413fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,6 @@ """Test configuration for py.test.""" +from __future__ import annotations + import sys import flake8 diff --git a/tests/integration/subdir/aplugin.py b/tests/integration/subdir/aplugin.py index 801f2c0..97b06a9 100644 --- a/tests/integration/subdir/aplugin.py +++ b/tests/integration/subdir/aplugin.py @@ -1,4 +1,5 @@ """Module that is off sys.path by default, for testing local-plugin-paths.""" +from __future__ import annotations class ExtensionTestPlugin2: diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py index d35266f..a5b39d7 100644 --- a/tests/integration/test_aggregator.py +++ b/tests/integration/test_aggregator.py @@ -1,4 +1,6 @@ """Test aggregation of config files and command-line options.""" +from __future__ import annotations + import os import pytest diff --git a/tests/integration/test_api_legacy.py b/tests/integration/test_api_legacy.py index efb0fc9..b386bd5 100644 --- a/tests/integration/test_api_legacy.py +++ b/tests/integration/test_api_legacy.py @@ -1,4 +1,6 @@ """Integration tests for the legacy api.""" +from __future__ import annotations + from flake8.api import legacy diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 9223ec4..ab9eb27 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -1,4 +1,6 @@ """Integration tests for the checker submodule.""" +from __future__ import annotations + import sys from unittest import mock diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index fe254b7..e711fb3 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -1,4 +1,6 @@ """Integration tests for the main entrypoint of flake8.""" +from __future__ import annotations + import json import os import sys diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 4323d05..0b4424a 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -1,4 +1,6 @@ """Integration tests for plugin loading.""" +from __future__ import annotations + import pytest from flake8.main.cli import main diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 904366e..0f8386a 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,4 +1,6 @@ """Shared fixtures between unit tests.""" +from __future__ import annotations + import argparse import pytest diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 63f8156..d526fd1 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import configparser import sys from unittest import mock diff --git a/tests/unit/plugins/pycodestyle_test.py b/tests/unit/plugins/pycodestyle_test.py index 703970f..1b00d9d 100644 --- a/tests/unit/plugins/pycodestyle_test.py +++ b/tests/unit/plugins/pycodestyle_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import importlib.machinery import importlib.util import os.path diff --git a/tests/unit/plugins/reporter_test.py b/tests/unit/plugins/reporter_test.py index 4b46cc4..07b0dfa 100644 --- a/tests/unit/plugins/reporter_test.py +++ b/tests/unit/plugins/reporter_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import pytest diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 508f83b..04147ec 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -1,4 +1,6 @@ """Tests for the Application class.""" +from __future__ import annotations + import argparse import pytest diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py index 7830eb4..5b57335 100644 --- a/tests/unit/test_base_formatter.py +++ b/tests/unit/test_base_formatter.py @@ -1,4 +1,6 @@ """Tests for the BaseFormatter object.""" +from __future__ import annotations + import argparse import sys from unittest import mock diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index c6114f6..32bc9a4 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -1,4 +1,6 @@ """Tests for the Manager object for FileCheckers.""" +from __future__ import annotations + import errno import multiprocessing from unittest import mock diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index 1fc93ef..6d8806c 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock from flake8._compat import importlib_metadata diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index 59c372a..27ced3f 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -1,4 +1,6 @@ """Tests for the flake8.style_guide.DecisionEngine class.""" +from __future__ import annotations + import argparse import pytest diff --git a/tests/unit/test_discover_files.py b/tests/unit/test_discover_files.py index f2dfa13..edf047d 100644 --- a/tests/unit/test_discover_files.py +++ b/tests/unit/test_discover_files.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 96d0244..99b298b 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -1,4 +1,6 @@ """Tests for the flake8.exceptions module.""" +from __future__ import annotations + import pickle import pytest diff --git a/tests/unit/test_file_checker.py b/tests/unit/test_file_checker.py index 3fe2e51..368b5f6 100644 --- a/tests/unit/test_file_checker.py +++ b/tests/unit/test_file_checker.py @@ -1,4 +1,6 @@ """Unit tests for the FileChecker class.""" +from __future__ import annotations + import argparse from unittest import mock diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index e8ebbc1..bd693b3 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -1,4 +1,6 @@ """Tests for the FileProcessor class.""" +from __future__ import annotations + import ast import tokenize from unittest import mock diff --git a/tests/unit/test_filenameonly_formatter.py b/tests/unit/test_filenameonly_formatter.py index e92d4bb..77f75b9 100644 --- a/tests/unit/test_filenameonly_formatter.py +++ b/tests/unit/test_filenameonly_formatter.py @@ -1,4 +1,6 @@ """Tests for the FilenameOnly formatter object.""" +from __future__ import annotations + import argparse from flake8.formatting import default diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 0e5b535..844bd5a 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -1,4 +1,6 @@ """Tests for Flake8's legacy API.""" +from __future__ import annotations + import argparse import configparser import os.path diff --git a/tests/unit/test_main_options.py b/tests/unit/test_main_options.py index aea2071..7c1feba 100644 --- a/tests/unit/test_main_options.py +++ b/tests/unit/test_main_options.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from flake8.main import options diff --git a/tests/unit/test_nothing_formatter.py b/tests/unit/test_nothing_formatter.py index eb4b862..76929fd 100644 --- a/tests/unit/test_nothing_formatter.py +++ b/tests/unit/test_nothing_formatter.py @@ -1,4 +1,6 @@ """Tests for the Nothing formatter obbject.""" +from __future__ import annotations + import argparse from flake8.formatting import default diff --git a/tests/unit/test_option.py b/tests/unit/test_option.py index d576c49..4b3070d 100644 --- a/tests/unit/test_option.py +++ b/tests/unit/test_option.py @@ -1,4 +1,6 @@ """Unit tests for flake8.options.manager.Option.""" +from __future__ import annotations + import functools from unittest import mock diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index 0a56076..d5b88c3 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -1,4 +1,6 @@ """Unit tests for flake.options.manager.OptionManager.""" +from __future__ import annotations + import argparse import os from unittest import mock diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 4ad6acd..0890ea9 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import configparser import os.path from unittest import mock diff --git a/tests/unit/test_pyflakes_codes.py b/tests/unit/test_pyflakes_codes.py index c251721..4626e3d 100644 --- a/tests/unit/test_pyflakes_codes.py +++ b/tests/unit/test_pyflakes_codes.py @@ -1,4 +1,6 @@ """Tests of pyflakes monkey patches.""" +from __future__ import annotations + import ast import pyflakes diff --git a/tests/unit/test_statistics.py b/tests/unit/test_statistics.py index 03f3189..261f360 100644 --- a/tests/unit/test_statistics.py +++ b/tests/unit/test_statistics.py @@ -1,4 +1,6 @@ """Tests for the statistics module in Flake8.""" +from __future__ import annotations + import pytest from flake8 import statistics as stats diff --git a/tests/unit/test_style_guide.py b/tests/unit/test_style_guide.py index e4aaff2..94fcb26 100644 --- a/tests/unit/test_style_guide.py +++ b/tests/unit/test_style_guide.py @@ -1,4 +1,6 @@ """Tests for the flake8.style_guide.StyleGuide class.""" +from __future__ import annotations + import argparse from unittest import mock diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index fefe662..98a5a5d 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,4 +1,6 @@ """Tests for flake8's utils module.""" +from __future__ import annotations + import io import logging import os diff --git a/tests/unit/test_violation.py b/tests/unit/test_violation.py index 6b47691..a4a43da 100644 --- a/tests/unit/test_violation.py +++ b/tests/unit/test_violation.py @@ -1,4 +1,6 @@ """Tests for the flake8.violation.Violation class.""" +from __future__ import annotations + from unittest import mock import pytest diff --git a/tox.ini b/tox.ini index c0763c1..df5a75c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion=2.3.1 -envlist = py36,py37,py38,flake8,linters,docs +envlist = py37,py38,flake8,linters,docs [testenv] deps = From 3642af2dcad88107dac7dc8bda03b5ada36d58fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Aug 2022 00:38:58 +0000 Subject: [PATCH 132/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 5.0.3 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.3...5.0.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07a863e..0ca677a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: black args: [--line-length=79] - repo: https://github.com/PyCQA/flake8 - rev: 5.0.3 + rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 68455f0e8f03a33b31216b547a1b15358d145e5a Mon Sep 17 00:00:00 2001 From: Dragos Velicanu Date: Wed, 10 Aug 2022 08:32:56 -0700 Subject: [PATCH 133/257] fix documentation of color configuruation --- docs/source/user/options.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index ab333c8..d9f1221 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -193,7 +193,7 @@ Options and their Descriptions Possible options are ``auto``, ``always``, and ``never``. - This **can** be specified in config files. + This **can not** be specified in config files. When color is enabled, the following substitutions are enabled: @@ -208,12 +208,6 @@ Options and their Descriptions - ``%(white)s`` - ``%(reset)s`` - Example config file usage: - - .. code-block:: ini - - color = never - .. option:: --count From 2ee3c56d9340df57516ec1214a930114ee6a6405 Mon Sep 17 00:00:00 2001 From: Kai Mueller Date: Sun, 4 Sep 2022 15:33:18 +0000 Subject: [PATCH 134/257] Fix typo casae->case --- docs/source/plugin-development/registering-plugins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index 065bdd8..63e279e 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -120,7 +120,7 @@ it would look like:: X10 = flake8_example:ExamplePlugin -In this casae as well as the following case, your entry-point name acts as +In this case as well as the following case, your entry-point name acts as a prefix to the error codes produced by your plugin. If all of your plugin's error codes start with ``X1`` then it would look From e299674866aee0946057fbed75711c501c3e686e Mon Sep 17 00:00:00 2001 From: Kai Mueller Date: Sun, 4 Sep 2022 15:55:29 +0000 Subject: [PATCH 135/257] Document off-by-default feature Add a short section to the plugin development. See #1676 --- docs/source/plugin-development/registering-plugins.rst | 6 ++++++ docs/source/user/options.rst | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index 63e279e..ca74008 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -143,6 +143,12 @@ i.e., ``ABC`` is better than ``A`` but ``ABCD`` is invalid. *A 3 letters entry point prefix followed by 3 numbers (i.e.* ``ABC123`` *) is currently the longest allowed entry point name.* +.. _off-by-default: + +If your plugin is intended to be opt-in, it can set the attribute +``off_by_default = True``. Users of your plugin will then need to utilize +:ref:`enable-extensions` with your plugin's entry +point. .. _Entry Points: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index d9f1221..170c1e7 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -794,11 +794,13 @@ Options and their Descriptions flake8-typing-extensions +.. _option-enable-extensions: + .. option:: --enable-extensions= :ref:`Go back to index ` - Enable off-by-default extensions. + Enable :ref:`off-by-default` extensions. Plugins to |Flake8| have the option of registering themselves as off-by-default. These plugins will not be loaded unless enabled by this From 46b404aa1f016bd3311eadaa03458827eb439ac2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Sep 2022 01:17:10 +0000 Subject: [PATCH 136/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ce2935..88b235d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black args: [--line-length=79] From a3e31c2f44449fe4392edb1ea485c458f9b539fe Mon Sep 17 00:00:00 2001 From: wookie184 Date: Tue, 6 Sep 2022 18:29:02 +0100 Subject: [PATCH 137/257] Prefer extend-ignore over ignore in general examples --- docs/source/index.rst | 4 ++-- docs/source/user/configuration.rst | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 51b0189..f4c5938 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -60,11 +60,11 @@ If you only want to see the instances of a specific warning or error, you can flake8 --select E123,W503 path/to/code/ -Alternatively, if you want to *ignore* only one specific warning or error: +Alternatively, if you want to add a specific warning or error to *ignore*: .. code:: - flake8 --ignore E24,W504 path/to/code/ + flake8 --extend-ignore E203,W234 path/to/code/ Please read our user guide for more information about how to use and configure |Flake8|. diff --git a/docs/source/user/configuration.rst b/docs/source/user/configuration.rst index eca2667..70fc22b 100644 --- a/docs/source/user/configuration.rst +++ b/docs/source/user/configuration.rst @@ -90,7 +90,7 @@ Let's actually look at |Flake8|'s own configuration section: .. code-block:: ini [flake8] - ignore = D203 + extend-ignore = E203 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist max-complexity = 10 @@ -98,7 +98,7 @@ This is equivalent to: .. prompt:: bash - flake8 --ignore D203 \ + flake8 --extend-ignore E203 \ --exclude .git,__pycache__,docs/source/conf.py,old,build,dist \ --max-complexity 10 @@ -107,7 +107,7 @@ In our case, if we wanted to, we could also do .. code-block:: ini [flake8] - ignore = D203 + extend-ignore = E203 exclude = .git, __pycache__, @@ -122,7 +122,7 @@ This allows us to add comments for why we're excluding items, e.g. .. code-block:: ini [flake8] - ignore = D203 + extend-ignore = E203 exclude = # No need to traverse our git directory .git, @@ -190,7 +190,7 @@ look at a portion of a project's Flake8 configuration in their ``tox.ini``: # H404: multi line docstring should start without a leading new line # H405: multi line docstring summary not separated with an empty line # H501: Do not use self.__dict__ for string formatting - ignore = H101,H202,H233,H301,H306,H401,H403,H404,H405,H501 + extend-ignore = H101,H202,H233,H301,H306,H401,H403,H404,H405,H501 They use the comments to describe the check but they could also write this as: @@ -198,7 +198,7 @@ They use the comments to describe the check but they could also write this as: [flake8] # it's not a bug that we aren't using all of hacking - ignore = + extend-ignore = # H101: Use TODO(NAME) H101, # H202: assertRaises Exception too broad From 8c7c38bad9a238f0bec9bc9ec58e87e7cc22d5e7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Sep 2022 00:54:40 +0000 Subject: [PATCH 138/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.37.3 → v2.38.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v2.38.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 88b235d..f38f352 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade args: [--py37-plus] From 045923237e8bcfcf109ce0621485ba06182ccba0 Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Thu, 22 Sep 2022 20:28:48 -0400 Subject: [PATCH 139/257] fix documentation for the --count option It erroneously claimed that it set the application exit code to 1 if the count was greater than 1. However, this is false, because the --count option doesn't modify the error code at any time. If the count was greater than 1, then the exit code was already 1, even in the absence of --count, unless --exit-zero was used. This documentation bug resulted in people reading the `flake8 --help` output and believing that --count is mandatory in order to ensure that flake8 produces errors in automated processes (such as CI scripts) when flake8 violations are detected. --- src/flake8/main/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index d603232..9b374ab 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -159,8 +159,8 @@ def register_default_options(option_manager: OptionManager) -> None: "--count", action="store_true", parse_from_config=True, - help="Print total number of errors to standard output and " - "set the exit code to 1 if total is not empty.", + help="Print total number of errors to standard output after " + "all other output.", ) add_option( From eafb4d72540c371cb3b9f268e25bc7c2b8b3c4ae Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 22 Sep 2022 21:04:10 -0400 Subject: [PATCH 140/257] remove example help text from docs to avoid confusion --- docs/source/user/invocation.rst | 66 +-------------------------------- 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/docs/source/user/invocation.rst b/docs/source/user/invocation.rst index 590bcf8..0049ec9 100644 --- a/docs/source/user/invocation.rst +++ b/docs/source/user/invocation.rst @@ -86,69 +86,5 @@ And you should see something like: Options: --version show program's version number and exit -h, --help show this help message and exit - -v, --verbose Print more information about what is happening in - flake8. This option is repeatable and will increase - verbosity each time it is repeated. - -q, --quiet Report only file names, or nothing. This option is - repeatable. - --count Print total number of errors and warnings to standard - error and set the exit code to 1 if total is not - empty. - --diff Report changes only within line number ranges in the - unified diff provided on standard in by the user. - --exclude=patterns Comma-separated list of files or directories to - exclude.(Default: - .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.nox,.eggs, - *.egg) - --filename=patterns Only check for filenames matching the patterns in this - comma-separated list. (Default: *.py) - --format=format Format errors according to the chosen formatter. - --hang-closing Hang closing bracket instead of matching indentation - of opening bracket's line. - --ignore=errors Comma-separated list of errors and warnings to ignore - (or skip). For example, ``--ignore=E4,E51,W234``. - (Default: E121,E123,E126,E226,E24,E704) - --extend-ignore=errors - Comma-separated list of errors and warnings to add to - the list of ignored ones. For example, ``--extend- - ignore=E4,E51,W234``. - --max-line-length=n Maximum allowed line length for the entirety of this - run. (Default: 79) - --select=errors Comma-separated list of errors and warnings to enable. - For example, ``--select=E4,E51,W234``. (Default: ) - --extend-select errors - Comma-separated list of errors and warnings to add to - the list of selected ones. For example, ``--extend- - select=E4,E51,W234``. - --disable-noqa Disable the effect of "# noqa". This will report - errors on lines with "# noqa" at the end. - --show-source Show the source generate each error or warning. - --statistics Count errors and warnings. - --enabled-extensions=ENABLED_EXTENSIONS - Enable plugins and extensions that are otherwise - disabled by default - --exit-zero Exit with status code "0" even if there are errors. - -j JOBS, --jobs=JOBS Number of subprocesses to use to run checks in - parallel. This is ignored on Windows. The default, - "auto", will auto-detect the number of processors - available to use. (Default: auto) - --output-file=OUTPUT_FILE - Redirect report to a file. - --tee Write to stdout and output-file. - --append-config=APPEND_CONFIG - Provide extra config files to parse in addition to the - files found by Flake8 by default. These files are the - last ones read and so they take the highest precedence - when multiple files provide the same option. - --config=CONFIG Path to the config file that will be the authoritative - config source. This will cause Flake8 to ignore all - other configuration files. - --isolated Ignore all configuration files. - --builtins=BUILTINS define more built-ins, comma separated - --doctests check syntax of the doctests - --include-in-doctest=INCLUDE_IN_DOCTEST - Run doctests only on these files - --exclude-from-doctest=EXCLUDE_FROM_DOCTEST - Skip these files when running doctests - Installed plugins: pyflakes: 1.0.0, pep8: 1.7.0 + ... From d59c42769c3907f38adfeab0581be80d043c290b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Sep 2022 01:09:19 +0000 Subject: [PATCH 141/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.2 → v3.8.3](https://github.com/asottile/reorder_python_imports/compare/v3.8.2...v3.8.3) - [github.com/asottile/pyupgrade: v2.38.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.38.0...v2.38.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f38f352..1902447 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.2 + rev: v3.8.3 hooks: - id: reorder-python-imports args: [ @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.0 + rev: v2.38.2 hooks: - id: pyupgrade args: [--py37-plus] From 4bba8798ea427ff3ad472b990784144fb4b1e661 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Oct 2022 12:25:05 -0400 Subject: [PATCH 142/257] Add link to GitHub Sponsors for primary maintainer --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..eb54a96 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: asottile From b029e85b44eabe48c8e22d0bced180472fd3697b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 01:05:39 +0000 Subject: [PATCH 143/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.971 → v0.981](https://github.com/pre-commit/mirrors-mypy/compare/v0.971...v0.981) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1902447..5ae2954 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.981 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From df938795956da742246c930c463dff504676993a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 02:26:58 +0000 Subject: [PATCH 144/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.3 → v3.8.4](https://github.com/asottile/reorder_python_imports/compare/v3.8.3...v3.8.4) - [github.com/asottile/pyupgrade: v2.38.2 → v3.1.0](https://github.com/asottile/pyupgrade/compare/v2.38.2...v3.1.0) - [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) - [github.com/pre-commit/mirrors-mypy: v0.981 → v0.982](https://github.com/pre-commit/mirrors-mypy/compare/v0.981...v0.982) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ae2954..7e05447 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.3 + rev: v3.8.4 hooks: - id: reorder-python-imports args: [ @@ -17,12 +17,12 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.2 + rev: v3.1.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: [--line-length=79] @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.982 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 48b29191303b271a162b3acbdfba3e5cf310b50c Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Thu, 28 Jul 2022 22:06:58 +0200 Subject: [PATCH 145/257] Display list of available formatters with help for --format --- src/flake8/main/application.py | 1 + src/flake8/main/options.py | 10 +++++++++- src/flake8/options/manager.py | 2 ++ tests/integration/test_aggregator.py | 1 + tests/integration/test_main.py | 10 ++++++++++ tests/integration/test_plugins.py | 1 + tests/unit/test_option_manager.py | 10 ++++++++-- tests/unit/test_options_config.py | 4 +++- 8 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 13ece4e..5178abb 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -132,6 +132,7 @@ class Application: version=flake8.__version__, plugin_versions=self.plugins.versions_str(), parents=[self.prelim_arg_parser], + formatter_names=list(self.plugins.reporters), ) options.register_default_options(self.option_manager) self.option_manager.register_plugins(self.plugins) diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 9b374ab..86a6cf8 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -220,7 +220,15 @@ def register_default_options(option_manager: OptionManager) -> None: metavar="format", default="default", parse_from_config=True, - help="Format errors according to the chosen formatter.", + help=( + f"Format errors according to the chosen formatter " + f"({', '.join(sorted(option_manager.formatter_names))}) " + f"or a format string containing %%-style " + f"mapping keys (code, col, path, row, text). " + f"For example, " + f"``--format=pylint`` or ``--format='%%(path)s %%(code)s'``. " + f"(Default: %(default)s)" + ), ) add_option( diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index e333c9e..7c40cb9 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -317,6 +317,7 @@ class OptionManager: version: str, plugin_versions: str, parents: list[argparse.ArgumentParser], + formatter_names: list[str], ) -> None: """Initialize an instance of an OptionManager. @@ -330,6 +331,7 @@ class OptionManager: A list of ArgumentParser objects whose arguments should also be included. """ + self.formatter_names = formatter_names self.parser = argparse.ArgumentParser( prog="flake8", usage="%(prog)s [options] file file ...", diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py index a5b39d7..006ac5f 100644 --- a/tests/integration/test_aggregator.py +++ b/tests/integration/test_aggregator.py @@ -18,6 +18,7 @@ def optmanager(): version="3.0.0", plugin_versions="", parents=[], + formatter_names=[], ) options.register_default_options(option_manager) return option_manager diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index e711fb3..dfa0e0b 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -406,3 +406,13 @@ The specified config file does not exist: missing.cfg out, err = capsys.readouterr() assert out == expected assert err == "" + + +def test_format_option_help(capsys): + """Test that help displays list of available formatters.""" + with pytest.raises(SystemExit): + cli.main(["--help"]) + + out, err = capsys.readouterr() + assert "(default, pylint, quiet-filename, quiet-nothing)" in out + assert err == "" diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 0b4424a..edba048 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -100,6 +100,7 @@ def test_local_plugin_can_add_option(local_config): version="123", plugin_versions="", parents=[stage1_parser], + formatter_names=[], ) register_default_options(option_manager) option_manager.register_plugins(loaded_plugins) diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index d5b88c3..3d3ddc1 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -17,7 +17,10 @@ TEST_VERSION = "3.0.0b1" def optmanager(): """Generate a simple OptionManager with default test arguments.""" return manager.OptionManager( - version=TEST_VERSION, plugin_versions="", parents=[] + version=TEST_VERSION, + plugin_versions="", + parents=[], + formatter_names=[], ) @@ -34,7 +37,10 @@ def test_option_manager_including_parent_options(): # WHEN optmanager = manager.OptionManager( - version=TEST_VERSION, plugin_versions="", parents=[parent_parser] + version=TEST_VERSION, + plugin_versions="", + parents=[parent_parser], + formatter_names=[], ) options = optmanager.parse_args(["--parent", "foo"]) diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 0890ea9..8c8f0cb 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -168,7 +168,9 @@ def test_load_extra_config_utf8(tmpdir): @pytest.fixture def opt_manager(): - ret = OptionManager(version="123", plugin_versions="", parents=[]) + ret = OptionManager( + version="123", plugin_versions="", parents=[], formatter_names=[] + ) register_default_options(ret) return ret From 9b929478d19e9e9ae0235963a83485dfa0d937ca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 01:25:12 +0000 Subject: [PATCH 146/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.4 → v3.8.5](https://github.com/asottile/reorder_python_imports/compare/v3.8.4...v3.8.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e05447..567bc7a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.4 + rev: v3.8.5 hooks: - id: reorder-python-imports args: [ From 3ec725734559d384d5cbf59177740ba5c2773afc Mon Sep 17 00:00:00 2001 From: Bastien Gerard Date: Mon, 24 Oct 2022 22:17:51 +0200 Subject: [PATCH 147/257] remove docstring of OptionManager as typing is sufficient --- src/flake8/options/manager.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 7c40cb9..8b4ec19 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -319,18 +319,7 @@ class OptionManager: parents: list[argparse.ArgumentParser], formatter_names: list[str], ) -> None: - """Initialize an instance of an OptionManager. - - :param prog: - Name of the actual program (e.g., flake8). - :param version: - Version string for the program. - :param usage: - Basic usage string used by the OptionParser. - :param parents: - A list of ArgumentParser objects whose arguments should also be - included. - """ + """Initialize an instance of an OptionManager.""" self.formatter_names = formatter_names self.parser = argparse.ArgumentParser( prog="flake8", From fba6df88f92c1c5f29c68c501737758a66594d06 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Oct 2022 20:37:24 -0700 Subject: [PATCH 148/257] remove --diff option --- docs/source/internal/utils.rst | 8 -- docs/source/user/options.rst | 23 ----- src/flake8/api/legacy.py | 1 - src/flake8/checker.py | 10 -- src/flake8/discover_files.py | 25 ++--- src/flake8/main/application.py | 23 +---- src/flake8/main/options.py | 8 -- src/flake8/style_guide.py | 27 +----- src/flake8/utils.py | 67 ------------- src/flake8/violation.py | 33 ------- tests/fixtures/diffs/multi_file_diff | 130 -------------------------- tests/fixtures/diffs/single_file_diff | 27 ------ tests/fixtures/diffs/two_file_diff | 45 --------- tests/integration/test_main.py | 36 ------- tests/unit/test_checker_manager.py | 7 +- tests/unit/test_discover_files.py | 10 -- tests/unit/test_utils.py | 38 -------- tests/unit/test_violation.py | 19 ---- 18 files changed, 11 insertions(+), 526 deletions(-) delete mode 100644 tests/fixtures/diffs/multi_file_diff delete mode 100644 tests/fixtures/diffs/single_file_diff delete mode 100644 tests/fixtures/diffs/two_file_diff diff --git a/docs/source/internal/utils.rst b/docs/source/internal/utils.rst index c745917..dc53378 100644 --- a/docs/source/internal/utils.rst +++ b/docs/source/internal/utils.rst @@ -66,11 +66,3 @@ The standard library's :func:`fnmatch.fnmatch` is excellent at deciding if a filename matches a single pattern. In our use case, however, we typically have a list of patterns and want to know if the filename matches any of them. This function abstracts that logic away with a little extra logic. - -.. autofunction:: flake8.utils.parse_unified_diff - -To handle usage of :option:`flake8 --diff`, |Flake8| needs to be able -to parse the name of the files in the diff as well as the ranges indicated the -sections that have been changed. This function either accepts the diff as an -argument or reads the diff from standard-in. It then returns a dictionary with -filenames as the keys and sets of line numbers as the value. diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index 170c1e7..f9e8b86 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -44,8 +44,6 @@ Index of Options - :option:`flake8 --count` -- :option:`flake8 --diff` - - :option:`flake8 --exclude` - :option:`flake8 --filename` @@ -230,27 +228,6 @@ Options and their Descriptions count = True -.. option:: --diff - - :ref:`Go back to index ` - - .. warning:: - - Due to hiding potential errors, this option is deprecated and will be - removed in a future version. - - Use the unified diff provided on standard in to only check the modified - files and report errors included in the diff. - - Command-line example: - - .. prompt:: bash - - git diff -u | flake8 --diff - - This **can not** be specified in config files. - - .. option:: --exclude= :ref:`Go back to index ` diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 9635756..ba4339e 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -136,7 +136,6 @@ class StyleGuide: stdin_display_name=self.options.stdin_display_name, filename_patterns=self.options.filename, exclude=self.options.exclude, - is_running_from_diff=self.options.diff, ) ) return not paths diff --git a/src/flake8/checker.py b/src/flake8/checker.py index d73349f..18b1ff6 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -97,8 +97,6 @@ class Manager: # implementation issues # - the user provided stdin and that's not something we can handle # well - # - we're processing a diff, which again does not work well with - # multiprocessing and which really shouldn't require multiprocessing # - the user provided some awful input # class state is only preserved when using the `fork` strategy. @@ -116,13 +114,6 @@ class Manager: ) return 0 - if self.options.diff: - LOG.warning( - "The --diff option was specified with --jobs but " - "they are not compatible. Ignoring --jobs arguments." - ) - return 0 - jobs = self.options.jobs # If the value is "auto", we want to let the multiprocessing library @@ -169,7 +160,6 @@ class Manager: stdin_display_name=self.options.stdin_display_name, filename_patterns=self.options.filename, exclude=self.exclude, - is_running_from_diff=self.options.diff, ) ] self.checkers = [c for c in self._all_checkers if c.should_process] diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py index b8592c8..580d5fd 100644 --- a/src/flake8/discover_files.py +++ b/src/flake8/discover_files.py @@ -55,7 +55,6 @@ def expand_paths( stdin_display_name: str, filename_patterns: Sequence[str], exclude: Sequence[str], - is_running_from_diff: bool, ) -> Generator[str, None, None]: """Expand out ``paths`` from commandline to the lintable files.""" if not paths: @@ -75,24 +74,16 @@ def expand_paths( logger=LOG, ) - def is_included(arg: str, fname: str) -> bool: - # while running from a diff, the arguments aren't _explicitly_ - # listed so we still filter them - if is_running_from_diff: - return utils.fnmatch(fname, filename_patterns) - else: - return ( - # always lint `-` - fname == "-" - # always lint explicitly passed (even if not matching filter) - or arg == fname - # otherwise, check the file against filtered patterns - or utils.fnmatch(fname, filename_patterns) - ) - return ( filename for path in paths for filename in _filenames_from(path, predicate=is_excluded) - if is_included(path, filename) + if ( + # always lint `-` + filename == "-" + # always lint explicitly passed (even if not matching filter) + or path == filename + # otherwise, check the file against filtered patterns + or utils.fnmatch(filename, filename_patterns) + ) ) diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 5178abb..c76fb86 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -13,7 +13,6 @@ from flake8 import checker from flake8 import defaults from flake8 import exceptions from flake8 import style_guide -from flake8 import utils from flake8.formatting.base import BaseFormatter from flake8.main import debug from flake8.main import options @@ -66,9 +65,6 @@ class Application: #: with a non-zero status code self.catastrophic_failure = False - #: The parsed diff information - self.parsed_diff: dict[str, set[int]] = {} - def parse_preliminary_options( self, argv: Sequence[str] ) -> tuple[argparse.Namespace, list[str]]: @@ -158,13 +154,6 @@ class Application: print(json.dumps(info, indent=2, sort_keys=True)) raise SystemExit(0) - if self.options.diff: - LOG.warning( - "the --diff option is deprecated and will be removed in a " - "future version." - ) - self.parsed_diff = utils.parse_unified_diff() - for loaded in self.plugins.all_plugins(): parse_options = getattr(loaded.obj, "parse_options", None) if parse_options is None: @@ -194,9 +183,6 @@ class Application: self.options, self.formatter ) - if self.options.diff: - self.guide.add_diff_ranges(self.parsed_diff) - def make_file_checker_manager(self) -> None: """Initialize our FileChecker Manager.""" assert self.guide is not None @@ -213,16 +199,9 @@ class Application: :class:`~flake8.checker.Manger` instance run the checks it is managing. """ - assert self.options is not None assert self.file_checker_manager is not None - if self.options.diff: - files: list[str] | None = sorted(self.parsed_diff) - if not files: - return - else: - files = None - self.file_checker_manager.start(files) + self.file_checker_manager.start() try: self.file_checker_manager.run() except exceptions.PluginExecutionFailed as plugin_failed: diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 86a6cf8..891bae9 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -114,7 +114,6 @@ def register_default_options(option_manager: OptionManager) -> None: - ``-q``/``--quiet`` - ``--color`` - ``--count`` - - ``--diff`` - ``--exclude`` - ``--extend-exclude`` - ``--filename`` @@ -163,13 +162,6 @@ def register_default_options(option_manager: OptionManager) -> None: "all other output.", ) - add_option( - "--diff", - action="store_true", - help="(DEPRECATED) Report changes only within line number ranges in " - "the unified diff provided on standard in by the user.", - ) - add_option( "--exclude", metavar="patterns", diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 2fee0f3..94698f8 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -298,18 +298,6 @@ class StyleGuideManager: code, filename, line_number, column_number, text, physical_line ) - def add_diff_ranges(self, diffinfo: 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 - the errors in the line number ranges are reported. - - :param diffinfo: - Dictionary mapping filenames to sets of line number ranges. - """ - for guide in self.style_guides: - guide.add_diff_ranges(diffinfo) - class StyleGuide: """Manage a Flake8 user's style guide.""" @@ -333,7 +321,6 @@ class StyleGuide: self.filename = filename if self.filename: self.filename = utils.normalize_path(self.filename) - self._parsed_diff: dict[str, set[int]] = {} def __repr__(self) -> str: """Make it easier to debug which StyleGuide we're using.""" @@ -440,20 +427,8 @@ class StyleGuide: self.should_report_error(error.code) is Decision.Selected ) is_not_inline_ignored = error.is_inline_ignored(disable_noqa) is False - is_included_in_diff = error.is_in(self._parsed_diff) - if error_is_selected and is_not_inline_ignored and is_included_in_diff: + if error_is_selected and is_not_inline_ignored: self.formatter.handle(error) self.stats.record(error) return 1 return 0 - - def add_diff_ranges(self, diffinfo: 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 - in the line number ranges are reported. - - :param diffinfo: - Dictionary mapping filenames to sets of line number ranges. - """ - self._parsed_diff = diffinfo diff --git a/src/flake8/utils.py b/src/flake8/utils.py index 60555a9..afc3896 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -1,7 +1,6 @@ """Utility methods for flake8.""" from __future__ import annotations -import collections import fnmatch as _fnmatch import functools import io @@ -18,7 +17,6 @@ from typing import Sequence from flake8 import exceptions -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]") NORMALIZE_PACKAGE_NAME_RE = re.compile(r"[-_.]+") @@ -202,71 +200,6 @@ def stdin_get_lines() -> list[str]: return list(io.StringIO(stdin_get_value())) -def parse_unified_diff(diff: str | None = None) -> dict[str, set[int]]: - """Parse the unified diff passed on stdin. - - :returns: - dictionary mapping file names to sets of line numbers - """ - # Allow us to not have to patch out stdin_get_value - if diff is None: - diff = stdin_get_value() - - number_of_rows = None - current_path = None - parsed_paths: dict[str, set[int]] = collections.defaultdict(set) - for line in diff.splitlines(): - if number_of_rows: - if not line or line[0] != "-": - number_of_rows -= 1 - # We're in the part of the diff that has lines starting with +, -, - # and ' ' to show context and the changes made. We skip these - # because the information we care about is the filename and the - # range within it. - # When number_of_rows reaches 0, we will once again start - # searching for filenames and ranges. - continue - - # NOTE(sigmavirus24): Diffs that we support look roughly like: - # diff a/file.py b/file.py - # ... - # --- a/file.py - # +++ b/file.py - # Below we're looking for that last line. Every diff tool that - # gives us this output may have additional information after - # ``b/file.py`` which it will separate with a \t, e.g., - # +++ b/file.py\t100644 - # Which is an example that has the new file permissions/mode. - # In this case we only care about the file name. - if line[:3] == "+++": - current_path = line[4:].split("\t", 1)[0] - # NOTE(sigmavirus24): This check is for diff output from git. - if current_path[:2] == "b/": - current_path = current_path[2:] - # We don't need to do anything else. We have set up our local - # ``current_path`` variable. We can skip the rest of this loop. - # The next line we will see will give us the hung information - # which is in the next section of logic. - continue - - hunk_match = DIFF_HUNK_REGEXP.match(line) - # NOTE(sigmavirus24): pep8/pycodestyle check for: - # line[:3] == '@@ ' - # But the DIFF_HUNK_REGEXP enforces that the line start with that - # So we can more simply check for a match instead of slicing and - # comparing. - if hunk_match: - (row, number_of_rows) = ( - 1 if not group else int(group) for group in hunk_match.groups() - ) - assert current_path is not None - parsed_paths[current_path].update(range(row, row + number_of_rows)) - - # We have now parsed our diff into a dictionary that looks like: - # {'file.py': set(range(10, 16), range(18, 20)), ...} - return parsed_paths - - def is_using_stdin(paths: list[str]) -> bool: """Determine if we're going to read from stdin. diff --git a/src/flake8/violation.py b/src/flake8/violation.py index 45834b2..96161d4 100644 --- a/src/flake8/violation.py +++ b/src/flake8/violation.py @@ -67,36 +67,3 @@ class Violation(NamedTuple): "%r is not ignored inline with ``# noqa: %s``", self, codes_str ) return False - - def is_in(self, diff: dict[str, set[int]]) -> bool: - """Determine if the violation is included in a diff's line ranges. - - This function relies on the parsed data added via - :meth:`~StyleGuide.add_diff_ranges`. If that has not been called and - we are not evaluating files in a diff, then this will always return - True. If there are diff ranges, then this will return True if the - line number in the error falls inside one of the ranges for the file - (and assuming the file is part of the diff data). If there are diff - ranges, this will return False if the file is not part of the diff - data or the line number of the error is not in any of the ranges of - the diff. - - :returns: - True if there is no diff or if the error is in the diff's line - number ranges. False if the error's line number falls outside - the diff's line number ranges. - """ - if not diff: - return True - - # NOTE(sigmavirus24): The parsed diff will be a defaultdict with - # a set as the default value (if we have received it from - # flake8.utils.parse_unified_diff). In that case ranges below - # could be an empty set (which is False-y) or if someone else - # is using this API, it could be None. If we could guarantee one - # or the other, we would check for it more explicitly. - line_numbers = diff.get(self.filename) - if not line_numbers: - return False - - return self.line_number in line_numbers diff --git a/tests/fixtures/diffs/multi_file_diff b/tests/fixtures/diffs/multi_file_diff deleted file mode 100644 index de86209..0000000 --- a/tests/fixtures/diffs/multi_file_diff +++ /dev/null @@ -1,130 +0,0 @@ -diff --git a/flake8/utils.py b/flake8/utils.py -index f6ce384..7cd12b0 100644 ---- a/flake8/utils.py -+++ b/flake8/utils.py -@@ -75,8 +75,8 @@ def stdin_get_value(): - return cached_value.getvalue() - - --def parse_unified_diff(): -- # type: () -> List[str] -+def parse_unified_diff(diff=None): -+ # type: (str) -> List[str] - """Parse the unified diff passed on stdin. - - :returns: -@@ -84,7 +84,10 @@ def parse_unified_diff(): - :rtype: - dict - """ -- diff = stdin_get_value() -+ # Allow us to not have to patch out stdin_get_value -+ if diff is None: -+ diff = stdin_get_value() -+ - number_of_rows = None - current_path = None - parsed_paths = collections.defaultdict(set) -diff --git a/tests/fixtures/diffs/single_file_diff b/tests/fixtures/diffs/single_file_diff -new file mode 100644 -index 0000000..77ca534 ---- /dev/null -+++ b/tests/fixtures/diffs/single_file_diff -@@ -0,0 +1,27 @@ -+diff --git a/flake8/utils.py b/flake8/utils.py -+index f6ce384..7cd12b0 100644 -+--- a/flake8/utils.py -++++ b/flake8/utils.py -+@@ -75,8 +75,8 @@ def stdin_get_value(): -+ return cached_value.getvalue() -+ -+ -+-def parse_unified_diff(): -+- # type: () -> List[str] -++def parse_unified_diff(diff=None): -++ # type: (str) -> List[str] -+ """Parse the unified diff passed on stdin. -+ -+ :returns: -+@@ -84,7 +84,10 @@ def parse_unified_diff(): -+ :rtype: -+ dict -+ """ -+- diff = stdin_get_value() -++ # Allow us to not have to patch out stdin_get_value -++ if diff is None: -++ diff = stdin_get_value() -++ -+ number_of_rows = None -+ current_path = None -+ parsed_paths = collections.defaultdict(set) -diff --git a/tests/fixtures/diffs/two_file_diff b/tests/fixtures/diffs/two_file_diff -new file mode 100644 -index 0000000..5bd35cd ---- /dev/null -+++ b/tests/fixtures/diffs/two_file_diff -@@ -0,0 +1,45 @@ -+diff --git a/flake8/utils.py b/flake8/utils.py -+index f6ce384..7cd12b0 100644 -+--- a/flake8/utils.py -++++ b/flake8/utils.py -+@@ -75,8 +75,8 @@ def stdin_get_value(): -+ return cached_value.getvalue() -+ -+ -+-def parse_unified_diff(): -+- # type: () -> List[str] -++def parse_unified_diff(diff=None): -++ # type: (str) -> List[str] -+ """Parse the unified diff passed on stdin. -+ -+ :returns: -+@@ -84,7 +84,10 @@ def parse_unified_diff(): -+ :rtype: -+ dict -+ """ -+- diff = stdin_get_value() -++ # Allow us to not have to patch out stdin_get_value -++ if diff is None: -++ diff = stdin_get_value() -++ -+ number_of_rows = None -+ current_path = None -+ parsed_paths = collections.defaultdict(set) -+diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py -+index d69d939..21482ce 100644 -+--- a/tests/unit/test_utils.py -++++ b/tests/unit/test_utils.py -+@@ -115,3 +115,13 @@ def test_parameters_for_function_plugin(): -+ plugin = plugin_manager.Plugin('plugin-name', object()) -+ plugin._plugin = fake_plugin -+ assert utils.parameters_for(plugin) == ['physical_line', 'self', 'tree'] -++ -++ -++def read_diff_file(filename): -++ """Read the diff file in its entirety.""" -++ with open(filename, 'r') as fd: -++ content = fd.read() -++ return content -++ -++ -++SINGLE_FILE_DIFF = read_diff_file('tests/fixtures/diffs/single_file_diff') -diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py -index d69d939..1461369 100644 ---- a/tests/unit/test_utils.py -+++ b/tests/unit/test_utils.py -@@ -115,3 +115,14 @@ def test_parameters_for_function_plugin(): - plugin = plugin_manager.Plugin('plugin-name', object()) - plugin._plugin = fake_plugin - assert utils.parameters_for(plugin) == ['physical_line', 'self', 'tree'] -+ -+ -+def read_diff_file(filename): -+ """Read the diff file in its entirety.""" -+ with open(filename, 'r') as fd: -+ content = fd.read() -+ return content -+ -+ -+SINGLE_FILE_DIFF = read_diff_file('tests/fixtures/diffs/single_file_diff') -+TWO_FILE_DIFF = read_diff_file('tests/fixtures/diffs/two_file_diff') diff --git a/tests/fixtures/diffs/single_file_diff b/tests/fixtures/diffs/single_file_diff deleted file mode 100644 index 77ca534..0000000 --- a/tests/fixtures/diffs/single_file_diff +++ /dev/null @@ -1,27 +0,0 @@ -diff --git a/flake8/utils.py b/flake8/utils.py -index f6ce384..7cd12b0 100644 ---- a/flake8/utils.py -+++ b/flake8/utils.py -@@ -75,8 +75,8 @@ def stdin_get_value(): - return cached_value.getvalue() - - --def parse_unified_diff(): -- # type: () -> List[str] -+def parse_unified_diff(diff=None): -+ # type: (str) -> List[str] - """Parse the unified diff passed on stdin. - - :returns: -@@ -84,7 +84,10 @@ def parse_unified_diff(): - :rtype: - dict - """ -- diff = stdin_get_value() -+ # Allow us to not have to patch out stdin_get_value -+ if diff is None: -+ diff = stdin_get_value() -+ - number_of_rows = None - current_path = None - parsed_paths = collections.defaultdict(set) diff --git a/tests/fixtures/diffs/two_file_diff b/tests/fixtures/diffs/two_file_diff deleted file mode 100644 index 5bd35cd..0000000 --- a/tests/fixtures/diffs/two_file_diff +++ /dev/null @@ -1,45 +0,0 @@ -diff --git a/flake8/utils.py b/flake8/utils.py -index f6ce384..7cd12b0 100644 ---- a/flake8/utils.py -+++ b/flake8/utils.py -@@ -75,8 +75,8 @@ def stdin_get_value(): - return cached_value.getvalue() - - --def parse_unified_diff(): -- # type: () -> List[str] -+def parse_unified_diff(diff=None): -+ # type: (str) -> List[str] - """Parse the unified diff passed on stdin. - - :returns: -@@ -84,7 +84,10 @@ def parse_unified_diff(): - :rtype: - dict - """ -- diff = stdin_get_value() -+ # Allow us to not have to patch out stdin_get_value -+ if diff is None: -+ diff = stdin_get_value() -+ - number_of_rows = None - current_path = None - parsed_paths = collections.defaultdict(set) -diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py -index d69d939..21482ce 100644 ---- a/tests/unit/test_utils.py -+++ b/tests/unit/test_utils.py -@@ -115,3 +115,13 @@ def test_parameters_for_function_plugin(): - plugin = plugin_manager.Plugin('plugin-name', object()) - plugin._plugin = fake_plugin - assert utils.parameters_for(plugin) == ['physical_line', 'self', 'tree'] -+ -+ -+def read_diff_file(filename): -+ """Read the diff file in its entirety.""" -+ with open(filename, 'r') as fd: -+ content = fd.read() -+ return content -+ -+ -+SINGLE_FILE_DIFF = read_diff_file('tests/fixtures/diffs/single_file_diff') diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index dfa0e0b..db60f6b 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -13,42 +13,6 @@ from flake8.main import cli from flake8.options import config -def test_diff_option(tmpdir, capsys): - """Ensure that `flake8 --diff` works.""" - t_py_contents = """\ -import os -import sys # unused but not part of diff - -print('(to avoid trailing whitespace in test)') -print('(to avoid trailing whitespace in test)') -print(os.path.join('foo', 'bar')) - -y # part of the diff and an error -""" - - diff = """\ -diff --git a/t.py b/t.py -index d64ac39..7d943de 100644 ---- a/t.py -+++ b/t.py -@@ -4,3 +4,5 @@ import sys # unused but not part of diff - print('(to avoid trailing whitespace in test)') - print('(to avoid trailing whitespace in test)') - print(os.path.join('foo', 'bar')) -+ -+y # part of the diff and an error -""" - - with mock.patch.object(utils, "stdin_get_value", return_value=diff): - with tmpdir.as_cwd(): - tmpdir.join("t.py").write(t_py_contents) - assert cli.main(["--diff"]) == 1 - - out, err = capsys.readouterr() - assert out == "t.py:8:1: F821 undefined name 'y'\n" - assert err == "" - - def test_form_feed_line_split(tmpdir, capsys): """Test that form feed is treated the same for stdin.""" src = "x=1\n\f\ny=1\n" diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 32bc9a4..b75bf6d 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -14,12 +14,7 @@ from flake8.plugins import finder def style_guide_mock(): """Create a mock StyleGuide object.""" - return mock.MagicMock( - **{ - "options.diff": False, - "options.jobs": JobsArgument("4"), - } - ) + return mock.MagicMock(**{"options.jobs": JobsArgument("4")}) def _parallel_checker_manager(): diff --git a/tests/unit/test_discover_files.py b/tests/unit/test_discover_files.py index edf047d..ca945c2 100644 --- a/tests/unit/test_discover_files.py +++ b/tests/unit/test_discover_files.py @@ -125,7 +125,6 @@ def _expand_paths( stdin_display_name="stdin", filename_patterns=("*.py",), exclude=(), - is_running_from_diff=False, ): return set( expand_paths( @@ -133,7 +132,6 @@ def _expand_paths( stdin_display_name=stdin_display_name, filename_patterns=filename_patterns, exclude=exclude, - is_running_from_diff=is_running_from_diff, ) ) @@ -166,11 +164,3 @@ def test_alternate_stdin_name_is_filtered(): def test_filename_included_even_if_not_matching_include(tmp_path): some_file = str(tmp_path.joinpath("some/file")) assert _expand_paths(paths=(some_file,)) == {some_file} - - -def test_diff_filenames_filtered_by_patterns(tmp_path): - f1 = str(tmp_path.joinpath("f1")) - f2 = str(tmp_path.joinpath("f2.py")) - - ret = _expand_paths(paths=(f1, f2), is_running_from_diff=True) - assert ret == {f2} diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 98a5a5d..82eef63 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -183,44 +183,6 @@ def test_fnmatch(filename, patterns, expected): assert utils.fnmatch(filename, patterns) is expected -def read_diff_file(filename): - """Read the diff file in its entirety.""" - with open(filename) as fd: - content = fd.read() - return content - - -SINGLE_FILE_DIFF = read_diff_file("tests/fixtures/diffs/single_file_diff") -SINGLE_FILE_INFO = { - "flake8/utils.py": set(range(75, 83)).union(set(range(84, 94))), -} -TWO_FILE_DIFF = read_diff_file("tests/fixtures/diffs/two_file_diff") -TWO_FILE_INFO = { - "flake8/utils.py": set(range(75, 83)).union(set(range(84, 94))), - "tests/unit/test_utils.py": set(range(115, 128)), -} -MULTI_FILE_DIFF = read_diff_file("tests/fixtures/diffs/multi_file_diff") -MULTI_FILE_INFO = { - "flake8/utils.py": set(range(75, 83)).union(set(range(84, 94))), - "tests/unit/test_utils.py": set(range(115, 129)), - "tests/fixtures/diffs/single_file_diff": set(range(1, 28)), - "tests/fixtures/diffs/two_file_diff": set(range(1, 46)), -} - - -@pytest.mark.parametrize( - "diff, parsed_diff", - [ - (SINGLE_FILE_DIFF, SINGLE_FILE_INFO), - (TWO_FILE_DIFF, TWO_FILE_INFO), - (MULTI_FILE_DIFF, MULTI_FILE_INFO), - ], -) -def test_parse_unified_diff(diff, parsed_diff): - """Verify that what we parse from a diff matches expectations.""" - assert utils.parse_unified_diff(diff) == parsed_diff - - def test_stdin_get_value_crlf(): """Ensure that stdin is normalized from crlf to lf.""" stdin = io.TextIOWrapper(io.BytesIO(b"1\r\n2\r\n"), "UTF-8") diff --git a/tests/unit/test_violation.py b/tests/unit/test_violation.py index a4a43da..1b4852b 100644 --- a/tests/unit/test_violation.py +++ b/tests/unit/test_violation.py @@ -51,22 +51,3 @@ def test_disable_is_inline_ignored(): assert error.is_inline_ignored(True) is False assert getline.called is False - - -@pytest.mark.parametrize( - "violation_file,violation_line,diff,expected", - [ - ("file.py", 10, {}, True), - ("file.py", 1, {"file.py": range(1, 2)}, True), - ("file.py", 10, {"file.py": range(1, 2)}, False), - ("file.py", 1, {"other.py": range(1, 2)}, False), - ("file.py", 10, {"other.py": range(1, 2)}, False), - ], -) -def test_violation_is_in_diff(violation_file, violation_line, diff, expected): - """Verify that we find violations within a diff.""" - violation = Violation( - "E001", violation_file, violation_line, 1, "warning", "line" - ) - - assert violation.is_in(diff) is expected From 1745fd4a88f463764b38e7f05b31ec2a64e0b7a7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Oct 2022 20:56:54 -0700 Subject: [PATCH 149/257] replace some unnecessary itertools.chain with *splat --- src/flake8/checker.py | 5 +---- src/flake8/style_guide.py | 11 ++++------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 18b1ff6..522d650 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -4,7 +4,6 @@ from __future__ import annotations import argparse import collections import errno -import itertools import logging import multiprocessing.pool import signal @@ -80,9 +79,7 @@ class Manager: "physical lines": 0, "tokens": 0, } - self.exclude = tuple( - itertools.chain(self.options.exclude, self.options.extend_exclude) - ) + self.exclude = (*self.options.exclude, *self.options.extend_exclude) def _process_statistics(self) -> None: for checker in self.checkers: diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 94698f8..8431650 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -6,7 +6,6 @@ import contextlib import copy import enum import functools -import itertools import logging from typing import Generator from typing import Sequence @@ -221,12 +220,10 @@ class StyleGuideManager: self.default_style_guide = StyleGuide( options, formatter, self.stats, decider=decider ) - self.style_guides = list( - itertools.chain( - [self.default_style_guide], - self.populate_style_guides_with(options), - ) - ) + self.style_guides = [ + self.default_style_guide, + *self.populate_style_guides_with(options), + ] self.style_guide_for = functools.lru_cache(maxsize=None)( self._style_guide_for From 8b1e7c696fa17dd23f025c1657fb97784ca859b4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Oct 2022 21:27:18 -0700 Subject: [PATCH 150/257] remove impossible None check --- src/flake8/processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 644192d..77f3d5a 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -326,7 +326,7 @@ class FileProcessor: def read_lines(self) -> list[str]: """Read the lines for this file checker.""" - if self.filename is None or self.filename == "-": + if self.filename == "-": self.filename = self.options.stdin_display_name or "stdin" lines = self.read_lines_from_stdin() else: From 0d667a73299971f1cf8ff549c519fffb282b1faf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Oct 2022 20:17:35 -0700 Subject: [PATCH 151/257] enable multiprocessing on other platforms --- src/flake8/api/legacy.py | 25 +---- src/flake8/checker.py | 171 +++++++++++++---------------- src/flake8/main/application.py | 133 ++-------------------- src/flake8/options/parse_args.py | 70 ++++++++++++ tests/integration/test_checker.py | 17 +-- tests/unit/test_checker_manager.py | 31 ++---- tests/unit/test_legacy_api.py | 45 -------- 7 files changed, 175 insertions(+), 317 deletions(-) create mode 100644 src/flake8/options/parse_args.py diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index ba4339e..446df29 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -10,11 +10,10 @@ import logging import os.path from typing import Any -import flake8 from flake8.discover_files import expand_paths from flake8.formatting import base as formatter from flake8.main import application as app -from flake8.options import config +from flake8.options.parse_args import parse_args LOG = logging.getLogger(__name__) @@ -163,7 +162,7 @@ class StyleGuide: # Stop cringing... I know it's gross. self._application.make_guide() self._application.file_checker_manager = None - self._application.make_file_checker_manager() + self._application.make_file_checker_manager([]) def input_file( self, @@ -200,23 +199,7 @@ def get_style_guide(**kwargs: Any) -> StyleGuide: An initialized StyleGuide """ application = app.Application() - prelim_opts, remaining_args = application.parse_preliminary_options([]) - flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) - - cfg, cfg_dir = config.load_config( - config=prelim_opts.config, - extra=prelim_opts.append_config, - isolated=prelim_opts.isolated, - ) - - application.find_plugins( - cfg, - cfg_dir, - enable_extensions=prelim_opts.enable_extensions, - require_plugins=prelim_opts.require_plugins, - ) - application.register_plugin_options() - application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) + application.plugins, application.options = parse_args([]) # We basically want application.initialize to be called but with these # options set instead before we make our formatter, notifier, internal # style guide and file checker manager. @@ -229,5 +212,5 @@ def get_style_guide(**kwargs: Any) -> StyleGuide: LOG.error('Could not update option "%s"', key) application.make_formatter() application.make_guide() - application.make_file_checker_manager() + application.make_file_checker_manager([]) return StyleGuide(application) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 522d650..2d7bcd9 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -2,15 +2,17 @@ from __future__ import annotations import argparse -import collections +import contextlib import errno import logging import multiprocessing.pool import signal import tokenize from typing import Any +from typing import Generator from typing import List from typing import Optional +from typing import Sequence from typing import Tuple from flake8 import defaults @@ -18,6 +20,7 @@ from flake8 import exceptions from flake8 import processor from flake8 import utils from flake8.discover_files import expand_paths +from flake8.options.parse_args import parse_args from flake8.plugins.finder import Checkers from flake8.plugins.finder import LoadedPlugin from flake8.style_guide import StyleGuideManager @@ -41,6 +44,41 @@ SERIAL_RETRY_ERRNOS = { # noise in diffs. } +_mp_plugins: Checkers +_mp_options: argparse.Namespace + + +@contextlib.contextmanager +def _mp_prefork( + plugins: Checkers, options: argparse.Namespace +) -> Generator[None, None, None]: + # we can save significant startup work w/ `fork` multiprocessing + global _mp_plugins, _mp_options + _mp_plugins, _mp_options = plugins, options + try: + yield + finally: + del _mp_plugins, _mp_options + + +def _mp_init(argv: Sequence[str]) -> None: + global _mp_plugins, _mp_options + + # Ensure correct signaling of ^C using multiprocessing.Pool. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + try: + _mp_plugins, _mp_options # for `fork` this'll already be set + except NameError: + plugins, options = parse_args(argv) + _mp_plugins, _mp_options = plugins.checkers, options + + +def _mp_run(filename: str) -> tuple[str, Results, dict[str, int]]: + return FileChecker( + filename=filename, plugins=_mp_plugins, options=_mp_options + ).run_checks() + class Manager: """Manage the parallelism and checker instances for each plugin and file. @@ -65,14 +103,13 @@ class Manager: self, style_guide: StyleGuideManager, plugins: Checkers, + argv: Sequence[str], ) -> None: """Initialize our Manager instance.""" self.style_guide = style_guide self.options = style_guide.options self.plugins = plugins self.jobs = self._job_count() - self._all_checkers: list[FileChecker] = [] - self.checkers: list[FileChecker] = [] self.statistics = { "files": 0, "logical lines": 0, @@ -80,30 +117,22 @@ class Manager: "tokens": 0, } self.exclude = (*self.options.exclude, *self.options.extend_exclude) + self.argv = argv + self.results: list[tuple[str, Results, dict[str, int]]] = [] def _process_statistics(self) -> None: - for checker in self.checkers: + for _, _, statistics in self.results: for statistic in defaults.STATISTIC_NAMES: - self.statistics[statistic] += checker.statistics[statistic] - self.statistics["files"] += len(self.checkers) + self.statistics[statistic] += statistics[statistic] + self.statistics["files"] += len(self.filenames) def _job_count(self) -> int: # First we walk through all of our error cases: # - multiprocessing library is not present - # - we're running on windows in which case we know we have significant - # implementation issues # - the user provided stdin and that's not something we can handle # well # - the user provided some awful input - # class state is only preserved when using the `fork` strategy. - if multiprocessing.get_start_method() != "fork": - LOG.warning( - "The multiprocessing module is not available. " - "Ignoring --jobs arguments." - ) - return 0 - if utils.is_using_stdin(self.options.filenames): LOG.warning( "The --jobs option is not compatible with supplying " @@ -141,27 +170,6 @@ class Manager: ) return reported_results_count - def make_checkers(self, paths: list[str] | None = None) -> None: - """Create checkers for each file.""" - if paths is None: - paths = self.options.filenames - - self._all_checkers = [ - FileChecker( - filename=filename, - plugins=self.plugins, - options=self.options, - ) - for filename in expand_paths( - paths=paths, - stdin_display_name=self.options.stdin_display_name, - filename_patterns=self.options.filename, - exclude=self.exclude, - ) - ] - self.checkers = [c for c in self._all_checkers if c.should_process] - LOG.info("Checking %d files", len(self.checkers)) - def report(self) -> tuple[int, int]: """Report all of the errors found in the managed file checkers. @@ -172,9 +180,8 @@ class Manager: A tuple of the total results found and the results reported. """ results_reported = results_found = 0 - for checker in self._all_checkers: - results = sorted(checker.results, key=lambda tup: (tup[1], tup[2])) - filename = checker.display_name + for filename, results, _ in self.results: + results.sort(key=lambda tup: (tup[1], tup[2])) with self.style_guide.processing_file(filename): results_reported += self._handle_results(filename, results) results_found += len(results) @@ -182,12 +189,8 @@ class Manager: def run_parallel(self) -> None: """Run the checkers in parallel.""" - # fmt: off - final_results: dict[str, list[tuple[str, int, int, str, str | None]]] = collections.defaultdict(list) # noqa: E501 - final_statistics: dict[str, dict[str, int]] = collections.defaultdict(dict) # noqa: E501 - # fmt: on - - pool = _try_initialize_processpool(self.jobs) + with _mp_prefork(self.plugins, self.options): + pool = _try_initialize_processpool(self.jobs, self.argv) if pool is None: self.run_serial() @@ -195,17 +198,7 @@ class Manager: pool_closed = False try: - pool_map = pool.imap_unordered( - _run_checks, - self.checkers, - chunksize=calculate_pool_chunksize( - len(self.checkers), self.jobs - ), - ) - for ret in pool_map: - filename, results, statistics = ret - final_results[filename] = results - final_statistics[filename] = statistics + self.results = list(pool.imap_unordered(_mp_run, self.filenames)) pool.close() pool.join() pool_closed = True @@ -214,15 +207,16 @@ class Manager: pool.terminate() pool.join() - for checker in self.checkers: - filename = checker.display_name - checker.results = final_results[filename] - checker.statistics = final_statistics[filename] - def run_serial(self) -> None: """Run the checkers in serial.""" - for checker in self.checkers: - checker.run_checks() + self.results = [ + FileChecker( + filename=filename, + plugins=self.plugins, + options=self.options, + ).run_checks() + for filename in self.filenames + ] def run(self) -> None: """Run all the checkers. @@ -234,7 +228,7 @@ class Manager: :issue:`117`) this also implements fallback to serial processing. """ try: - if self.jobs > 1 and len(self.checkers) > 1: + if self.jobs > 1 and len(self.filenames) > 1: self.run_parallel() else: self.run_serial() @@ -242,7 +236,7 @@ class Manager: LOG.warning("Flake8 was interrupted by the user") raise exceptions.EarlyQuit("Early quit while running checks") - def start(self, paths: list[str] | None = None) -> None: + def start(self) -> None: """Start checking files. :param paths: @@ -250,7 +244,14 @@ class Manager: :meth:`~Manager.make_checkers`. """ LOG.info("Making checkers") - self.make_checkers(paths) + self.filenames = tuple( + expand_paths( + paths=self.options.filenames, + stdin_display_name=self.options.stdin_display_name, + filename_patterns=self.options.filename, + exclude=self.exclude, + ) + ) def stop(self) -> None: """Stop checking files.""" @@ -325,7 +326,7 @@ class FileChecker: def run_check(self, plugin: LoadedPlugin, **arguments: Any) -> Any: """Run the check in a single plugin.""" - assert self.processor is not None + assert self.processor is not None, self.filename try: params = self.processor.keyword_arguments_for( plugin.parameters, arguments @@ -409,7 +410,7 @@ class FileChecker: def run_ast_checks(self) -> None: """Run all checks expecting an abstract syntax tree.""" - assert self.processor is not None + assert self.processor is not None, self.filename ast = self.processor.build_ast() for plugin in self.plugins.tree: @@ -514,7 +515,9 @@ class FileChecker: def run_checks(self) -> tuple[str, Results, dict[str, int]]: """Run checks against the file.""" - assert self.processor is not None + if self.processor is None or not self.should_process: + return self.display_name, self.results, self.statistics + try: self.run_ast_checks() self.process_tokens() @@ -522,11 +525,11 @@ class FileChecker: code = "E902" if isinstance(e, tokenize.TokenError) else "E999" row, column = self._extract_syntax_information(e) self.report(code, row, column, f"{type(e).__name__}: {e.args[0]}") - return self.filename, self.results, self.statistics + return self.display_name, self.results, self.statistics logical_lines = self.processor.statistics["logical lines"] self.statistics["logical lines"] = logical_lines - return self.filename, self.results, self.statistics + return self.display_name, self.results, self.statistics def handle_newline(self, token_type: int) -> None: """Handle the logic when encountering a newline token.""" @@ -573,17 +576,13 @@ class FileChecker: self.run_physical_checks(line) -def _pool_init() -> None: - """Ensure correct signaling of ^C using multiprocessing.Pool.""" - signal.signal(signal.SIGINT, signal.SIG_IGN) - - def _try_initialize_processpool( job_count: int, + argv: Sequence[str], ) -> multiprocessing.pool.Pool | None: """Return a new process pool instance if we are able to create one.""" try: - return multiprocessing.Pool(job_count, _pool_init) + return multiprocessing.Pool(job_count, _mp_init, initargs=(argv,)) except OSError as err: if err.errno not in SERIAL_RETRY_ERRNOS: raise @@ -593,22 +592,6 @@ def _try_initialize_processpool( return None -def calculate_pool_chunksize(num_checkers: int, num_jobs: int) -> int: - """Determine the chunksize for the multiprocessing Pool. - - - For chunksize, see: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap # noqa - - This formula, while not perfect, aims to give each worker two batches of - work. - - See: https://github.com/pycqa/flake8/issues/829#note_18878876 - - See: https://github.com/pycqa/flake8/issues/197 - """ - return max(num_checkers // (num_jobs * 2), 1) - - -def _run_checks(checker: FileChecker) -> tuple[str, Results, dict[str, int]]: - return checker.run_checks() - - def find_offset( offset: int, mapping: processor._LogicalMapping ) -> tuple[int, int]: diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index c76fb86..b6bfae3 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -2,7 +2,6 @@ from __future__ import annotations import argparse -import configparser import json import logging import time @@ -15,10 +14,7 @@ from flake8 import exceptions from flake8 import style_guide from flake8.formatting.base import BaseFormatter from flake8.main import debug -from flake8.main import options -from flake8.options import aggregator -from flake8.options import config -from flake8.options import manager +from flake8.options.parse_args import parse_args from flake8.plugins import finder from flake8.plugins import reporter @@ -35,12 +31,6 @@ class Application: self.start_time = time.time() #: The timestamp when the Application finished reported errors. self.end_time: float | None = None - #: The prelimary argument parser for handling options required for - #: obtaining and parsing the configuration file. - self.prelim_arg_parser = options.stage1_arg_parser() - #: The instance of :class:`flake8.options.manager.OptionManager` used - #: to parse and handle the options and arguments passed by the user - self.option_manager: manager.OptionManager | None = None self.plugins: finder.Plugins | None = None #: The user-selected formatter from :attr:`formatting_plugins` @@ -65,30 +55,6 @@ class Application: #: with a non-zero status code self.catastrophic_failure = False - def parse_preliminary_options( - self, argv: Sequence[str] - ) -> tuple[argparse.Namespace, list[str]]: - """Get preliminary options from the CLI, pre-plugin-loading. - - We need to know the values of a few standard options so that we can - locate configuration files and configure logging. - - Since plugins aren't loaded yet, there may be some as-yet-unknown - options; we ignore those for now, they'll be parsed later when we do - real option parsing. - - :param argv: - Command-line arguments passed in directly. - :returns: - Populated namespace and list of remaining argument strings. - """ - args, rest = self.prelim_arg_parser.parse_known_args(argv) - # XXX (ericvw): Special case "forwarding" the output file option so - # that it can be reparsed again for the BaseFormatter.filename. - if args.output_file: - rest.extend(("--output-file", args.output_file)) - return args, rest - def exit_code(self) -> int: """Return the program exit code.""" if self.catastrophic_failure: @@ -99,76 +65,6 @@ class Application: else: return int(self.result_count > 0) - def find_plugins( - self, - cfg: configparser.RawConfigParser, - cfg_dir: str, - *, - enable_extensions: str | None, - require_plugins: str | None, - ) -> None: - """Find and load the plugins for this application. - - Set :attr:`plugins` based on loaded plugins. - """ - opts = finder.parse_plugin_options( - cfg, - cfg_dir, - enable_extensions=enable_extensions, - require_plugins=require_plugins, - ) - raw = finder.find_plugins(cfg, opts) - self.plugins = finder.load_plugins(raw, opts) - - def register_plugin_options(self) -> None: - """Register options provided by plugins to our option manager.""" - assert self.plugins is not None - - self.option_manager = manager.OptionManager( - version=flake8.__version__, - plugin_versions=self.plugins.versions_str(), - parents=[self.prelim_arg_parser], - formatter_names=list(self.plugins.reporters), - ) - options.register_default_options(self.option_manager) - self.option_manager.register_plugins(self.plugins) - - def parse_configuration_and_cli( - self, - cfg: configparser.RawConfigParser, - cfg_dir: str, - argv: list[str], - ) -> None: - """Parse configuration files and the CLI options.""" - assert self.option_manager is not None - assert self.plugins is not None - self.options = aggregator.aggregate_options( - self.option_manager, - cfg, - cfg_dir, - argv, - ) - - if self.options.bug_report: - info = debug.information(flake8.__version__, self.plugins) - print(json.dumps(info, indent=2, sort_keys=True)) - raise SystemExit(0) - - for loaded in self.plugins.all_plugins(): - parse_options = getattr(loaded.obj, "parse_options", None) - if parse_options is None: - continue - - # XXX: ideally we wouldn't have two forms of parse_options - try: - parse_options( - self.option_manager, - self.options, - self.options.filenames, - ) - except TypeError: - parse_options(self.options) - def make_formatter(self) -> None: """Initialize a formatter based on the parsed options.""" assert self.plugins is not None @@ -183,13 +79,14 @@ class Application: self.options, self.formatter ) - def make_file_checker_manager(self) -> None: + def make_file_checker_manager(self, argv: Sequence[str]) -> None: """Initialize our FileChecker Manager.""" assert self.guide is not None assert self.plugins is not None self.file_checker_manager = checker.Manager( style_guide=self.guide, plugins=self.plugins.checkers, + argv=argv, ) def run_checks(self) -> None: @@ -265,28 +162,16 @@ class Application: This finds the plugins, registers their options, and parses the command-line arguments. """ - # NOTE(sigmavirus24): When updating this, make sure you also update - # our legacy API calls to these same methods. - prelim_opts, remaining_args = self.parse_preliminary_options(argv) - flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) + self.plugins, self.options = parse_args(argv) - cfg, cfg_dir = config.load_config( - config=prelim_opts.config, - extra=prelim_opts.append_config, - isolated=prelim_opts.isolated, - ) + if self.options.bug_report: + info = debug.information(flake8.__version__, self.plugins) + print(json.dumps(info, indent=2, sort_keys=True)) + raise SystemExit(0) - self.find_plugins( - cfg, - cfg_dir, - enable_extensions=prelim_opts.enable_extensions, - require_plugins=prelim_opts.require_plugins, - ) - self.register_plugin_options() - self.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) self.make_formatter() self.make_guide() - self.make_file_checker_manager() + self.make_file_checker_manager(argv) def report(self) -> None: """Report errors, statistics, and benchmarks.""" diff --git a/src/flake8/options/parse_args.py b/src/flake8/options/parse_args.py new file mode 100644 index 0000000..e3f8795 --- /dev/null +++ b/src/flake8/options/parse_args.py @@ -0,0 +1,70 @@ +"""Procedure for parsing args, config, loading plugins.""" +from __future__ import annotations + +import argparse +from typing import Sequence + +import flake8 +from flake8.main import options +from flake8.options import aggregator +from flake8.options import config +from flake8.options import manager +from flake8.plugins import finder + + +def parse_args( + argv: Sequence[str], +) -> tuple[finder.Plugins, argparse.Namespace]: + """Procedure for parsing args, config, loading plugins.""" + prelim_parser = options.stage1_arg_parser() + + args0, rest = prelim_parser.parse_known_args(argv) + # XXX (ericvw): Special case "forwarding" the output file option so + # that it can be reparsed again for the BaseFormatter.filename. + if args0.output_file: + rest.extend(("--output-file", args0.output_file)) + + flake8.configure_logging(args0.verbose, args0.output_file) + + cfg, cfg_dir = config.load_config( + config=args0.config, + extra=args0.append_config, + isolated=args0.isolated, + ) + + plugin_opts = finder.parse_plugin_options( + cfg, + cfg_dir, + enable_extensions=args0.enable_extensions, + require_plugins=args0.require_plugins, + ) + raw_plugins = finder.find_plugins(cfg, plugin_opts) + plugins = finder.load_plugins(raw_plugins, plugin_opts) + + option_manager = manager.OptionManager( + version=flake8.__version__, + plugin_versions=plugins.versions_str(), + parents=[prelim_parser], + formatter_names=list(plugins.reporters), + ) + options.register_default_options(option_manager) + option_manager.register_plugins(plugins) + + opts = aggregator.aggregate_options(option_manager, cfg, cfg_dir, rest) + + for loaded in plugins.all_plugins(): + parse_options = getattr(loaded.obj, "parse_options", None) + if parse_options is None: + continue + + # XXX: ideally we wouldn't have two forms of parse_options + try: + parse_options( + option_manager, + opts, + opts.filenames, + ) + except TypeError: + parse_options(opts) + + return plugins, opts diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index ab9eb27..13ec8c1 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -266,17 +266,12 @@ def test_report_order(results, expected_order): # tuples to create the expected result lists from the indexes expected_results = [results[index] for index in expected_order] - file_checker = mock.Mock(spec=["results", "display_name"]) - file_checker.results = results - file_checker.display_name = "placeholder" - style_guide = mock.MagicMock(spec=["options", "processing_file"]) # Create a placeholder manager without arguments or plugins # Just add one custom file checker which just provides the results - manager = checker.Manager(style_guide, finder.Checkers([], [], [])) - manager.checkers = manager._all_checkers = [file_checker] - + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + manager.results = [("placeholder", results, {})] # _handle_results is the first place which gets the sorted result # Should something non-private be mocked instead? handler = mock.Mock(side_effect=count_side_effect) @@ -295,9 +290,9 @@ def test_acquire_when_multiprocessing_pool_can_initialize(): This simulates the behaviour on most common platforms. """ with mock.patch("multiprocessing.Pool") as pool: - result = checker._try_initialize_processpool(2) + result = checker._try_initialize_processpool(2, []) - pool.assert_called_once_with(2, checker._pool_init) + pool.assert_called_once_with(2, checker._mp_init, initargs=([],)) assert result is pool.return_value @@ -314,9 +309,9 @@ def test_acquire_when_multiprocessing_pool_can_not_initialize(): https://github.com/python/cpython/blob/4e02981de0952f54bf87967f8e10d169d6946b40/Lib/multiprocessing/synchronize.py#L30-L33 """ with mock.patch("multiprocessing.Pool", side_effect=ImportError) as pool: - result = checker._try_initialize_processpool(2) + result = checker._try_initialize_processpool(2, []) - pool.assert_called_once_with(2, checker._pool_init) + pool.assert_called_once_with(2, checker._mp_init, initargs=([],)) assert result is None diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index b75bf6d..68dd82a 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -20,9 +20,9 @@ def style_guide_mock(): def _parallel_checker_manager(): """Call Manager.run() and return the number of calls to `run_serial`.""" style_guide = style_guide_mock() - manager = checker.Manager(style_guide, finder.Checkers([], [], [])) - # multiple checkers is needed for parallel mode - manager.checkers = [mock.Mock(), mock.Mock()] + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + # multiple files is needed for parallel mode + manager.filenames = ("file1", "file2") return manager @@ -36,8 +36,7 @@ def test_oserrors_cause_serial_fall_back(): assert serial.call_count == 1 -@mock.patch.object(multiprocessing, "get_start_method", return_value="fork") -def test_oserrors_are_reraised(_): +def test_oserrors_are_reraised(): """Verify that unexpected OSErrors will cause the Manager to reraise.""" err = OSError(errno.EAGAIN, "Ominous message") with mock.patch("_multiprocessing.SemLock", side_effect=err): @@ -48,14 +47,6 @@ def test_oserrors_are_reraised(_): assert serial.call_count == 0 -@mock.patch.object(multiprocessing, "get_start_method", return_value="spawn") -def test_multiprocessing_is_disabled(_): - """Verify not being able to import multiprocessing forces jobs to 0.""" - style_guide = style_guide_mock() - manager = checker.Manager(style_guide, finder.Checkers([], [], [])) - assert manager.jobs == 0 - - def test_multiprocessing_cpu_count_not_implemented(): """Verify that jobs is 0 if cpu_count is unavailable.""" style_guide = style_guide_mock() @@ -66,22 +57,18 @@ def test_multiprocessing_cpu_count_not_implemented(): "cpu_count", side_effect=NotImplementedError, ): - manager = checker.Manager(style_guide, finder.Checkers([], [], [])) + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) assert manager.jobs == 0 -@mock.patch.object(multiprocessing, "get_start_method", return_value="spawn") -def test_make_checkers(_): +def test_make_checkers(): """Verify that we create a list of FileChecker instances.""" style_guide = style_guide_mock() style_guide.options.filenames = ["file1", "file2"] - manager = checker.Manager(style_guide, finder.Checkers([], [], [])) + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) with mock.patch("flake8.utils.fnmatch", return_value=True): with mock.patch("flake8.processor.FileProcessor"): - manager.make_checkers(["file1", "file2"]) + manager.start() - assert manager._all_checkers - for file_checker in manager._all_checkers: - assert file_checker.filename in style_guide.options.filenames - assert not manager.checkers # the files don't exist + assert manager.filenames == ("file1", "file2") diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 844bd5a..c6af630 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -1,57 +1,12 @@ """Tests for Flake8's legacy API.""" from __future__ import annotations -import argparse -import configparser -import os.path from unittest import mock import pytest from flake8.api import legacy as api from flake8.formatting import base as formatter -from flake8.options import config - - -def test_get_style_guide(): - """Verify the methods called on our internal Application.""" - prelim_opts = argparse.Namespace( - append_config=[], - config=None, - isolated=False, - output_file=None, - verbose=0, - enable_extensions=None, - require_plugins=None, - ) - mockedapp = mock.Mock() - mockedapp.parse_preliminary_options.return_value = (prelim_opts, []) - mockedapp.program = "flake8" - - cfg = configparser.RawConfigParser() - cfg_dir = os.getcwd() - - with mock.patch.object(config, "load_config", return_value=(cfg, cfg_dir)): - with mock.patch("flake8.main.application.Application") as application: - application.return_value = mockedapp - style_guide = api.get_style_guide() - - application.assert_called_once_with() - mockedapp.parse_preliminary_options.assert_called_once_with([]) - mockedapp.find_plugins.assert_called_once_with( - cfg, - cfg_dir, - enable_extensions=None, - require_plugins=None, - ) - mockedapp.register_plugin_options.assert_called_once_with() - mockedapp.parse_configuration_and_cli.assert_called_once_with( - cfg, cfg_dir, [] - ) - mockedapp.make_formatter.assert_called_once_with() - mockedapp.make_guide.assert_called_once_with() - mockedapp.make_file_checker_manager.assert_called_once_with() - assert isinstance(style_guide, api.StyleGuide) def test_styleguide_options(): From eb66669067b0ca0e2beaa26a0643d9df82c3da08 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 28 Oct 2022 20:12:25 -0400 Subject: [PATCH 152/257] add tidelift security policy --- .github/SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..5885648 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,5 @@ +## security contact information + +to report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. From 449799975a309cb4ac5e5ce74ae11ec8f77c3a22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 28 Oct 2022 20:18:22 -0400 Subject: [PATCH 153/257] add tidelift to FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index eb54a96..7267456 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ github: asottile +tidelift: pypi/pre-commit From 7a094fa826aed5f0d2182744261b71a2f39c7396 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 30 Oct 2022 15:11:56 -0400 Subject: [PATCH 154/257] ensure results are sorted for file traversal --- src/flake8/checker.py | 4 +++- tests/integration/test_main.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 2d7bcd9..2cae545 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -6,6 +6,7 @@ import contextlib import errno import logging import multiprocessing.pool +import operator import signal import tokenize from typing import Any @@ -180,8 +181,9 @@ class Manager: A tuple of the total results found and the results reported. """ results_reported = results_found = 0 + self.results.sort(key=operator.itemgetter(0)) for filename, results, _ in self.results: - results.sort(key=lambda tup: (tup[1], tup[2])) + results.sort(key=operator.itemgetter(1, 2)) with self.style_guide.processing_file(filename): results_reported += self._handle_results(filename, results) results_found += len(results) diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index db60f6b..96d1182 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -98,6 +98,26 @@ t.py:1:1: F401 'os' imported but unused assert err == "" +def test_errors_sorted(tmpdir, capsys): + with tmpdir.as_cwd(): + for c in "abcde": + tmpdir.join(f"{c}.py").write("import os\n") + assert cli.main(["./"]) == 1 + + # file traversal was done in inode-order before + # this uses a significant number of files such that it's unlikely to pass + expected = """\ +./a.py:1:1: F401 'os' imported but unused +./b.py:1:1: F401 'os' imported but unused +./c.py:1:1: F401 'os' imported but unused +./d.py:1:1: F401 'os' imported but unused +./e.py:1:1: F401 'os' imported but unused +""" + out, err = capsys.readouterr() + assert out == expected + assert err == "" + + def test_extend_exclude(tmpdir, capsys): """Ensure that `flake8 --extend-exclude` works.""" for d in ["project", "vendor", "legacy", ".git", ".tox", ".hg"]: From 71ac5d1e8af015f1cc3d53f1dbb0265d187fb31a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Nov 2022 01:55:24 +0000 Subject: [PATCH 155/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.5 → v3.9.0](https://github.com/asottile/reorder_python_imports/compare/v3.8.5...v3.9.0) - [github.com/asottile/pyupgrade: v3.1.0 → v3.2.0](https://github.com/asottile/pyupgrade/compare/v3.1.0...v3.2.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 567bc7a..1a154c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.5 + rev: v3.9.0 hooks: - id: reorder-python-imports args: [ @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.2.0 hooks: - id: pyupgrade args: [--py37-plus] From 175c9ae3bcd505ce13b43ef2ec723f5eae3cbf26 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Sun, 6 Nov 2022 16:49:02 +0100 Subject: [PATCH 156/257] Fix typos --- src/flake8/plugins/pyflakes.py | 4 ++-- src/flake8/plugins/reporter.py | 2 +- src/flake8/processor.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 7b99fd4..cffea36 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -90,13 +90,13 @@ class FlakesChecker(pyflakes.checker.Checker): for exclude in self.exclude_from_doctest: if exclude != "" and filename.startswith(exclude): with_doctest = False - overlaped_by = [ + overlapped_by = [ include for include in included_by if include.startswith(exclude) ] - if overlaped_by: + if overlapped_by: with_doctest = True super().__init__( diff --git a/src/flake8/plugins/reporter.py b/src/flake8/plugins/reporter.py index f63b20c..a5749c0 100644 --- a/src/flake8/plugins/reporter.py +++ b/src/flake8/plugins/reporter.py @@ -1,4 +1,4 @@ -"""Functions for construcing the requested report plugin.""" +"""Functions for constructing the requested report plugin.""" from __future__ import annotations import argparse diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 77f3d5a..16fa16b 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -27,7 +27,7 @@ _Logical = Tuple[List[str], List[str], _LogicalMapping] class FileProcessor: - """Processes a file and holdes state. + """Processes a file and holds state. This processes a file by generating tokens, logical and physical lines, and AST trees. This also provides a way of passing state about the file @@ -63,7 +63,7 @@ class FileProcessor: options: argparse.Namespace, lines: list[str] | None = None, ) -> None: - """Initialice our file processor. + """Initialize our file processor. :param filename: Name of the file to process """ From 1346ddefd315223b8f476bb02381a944e14f2cbc Mon Sep 17 00:00:00 2001 From: Menno Liefstingh Date: Thu, 13 Oct 2022 01:15:35 +0200 Subject: [PATCH 157/257] Adds warning when invalid error codes are parsed for ignore or extend-ignore from config file --- src/flake8/defaults.py | 2 ++ src/flake8/options/config.py | 11 ++++++++++ src/flake8/plugins/finder.py | 8 +++---- tests/unit/plugins/finder_test.py | 31 -------------------------- tests/unit/test_defaults.py | 36 +++++++++++++++++++++++++++++++ tests/unit/test_options_config.py | 32 +++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 tests/unit/test_defaults.py diff --git a/src/flake8/defaults.py b/src/flake8/defaults.py index 4ba0048..e3edf32 100644 --- a/src/flake8/defaults.py +++ b/src/flake8/defaults.py @@ -43,3 +43,5 @@ NOQA_INLINE_REGEXP = re.compile( ) NOQA_FILE = re.compile(r"\s*# flake8[:=]\s*noqa", re.I) + +VALID_CODE_PREFIX = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index e158737..a9ced1b 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -7,6 +7,7 @@ import os.path from typing import Any from flake8 import exceptions +from flake8.defaults import VALID_CODE_PREFIX from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) @@ -120,6 +121,16 @@ def parse_config( LOG.debug('Option "%s" returned value: %r', option_name, value) final_value = option.normalize(value, cfg_dir) + + if option_name in {"ignore", "extend-ignore"}: + for error_code in final_value: + if not VALID_CODE_PREFIX.match(error_code): + raise ValueError( + f"Error code {error_code!r} " + f"supplied to {option_name!r} option " + f"does not match {VALID_CODE_PREFIX.pattern!r}" + ) + assert option.config_name is not None config_dict[option.config_name] = final_value diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index c051488..4a43ccb 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -5,7 +5,6 @@ import configparser import inspect import itertools import logging -import re import sys from typing import Any from typing import Generator @@ -14,13 +13,12 @@ from typing import NamedTuple from flake8 import utils from flake8._compat import importlib_metadata +from flake8.defaults import VALID_CODE_PREFIX from flake8.exceptions import ExecutionError from flake8.exceptions import FailedToLoadPlugin LOG = logging.getLogger(__name__) -VALID_CODE = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII) - FLAKE8_GROUPS = frozenset(("flake8.extension", "flake8.report")) BANNED_PLUGINS = { @@ -337,10 +335,10 @@ def _classify_plugins( raise NotImplementedError(f"what plugin type? {loaded}") for loaded in itertools.chain(tree, logical_line, physical_line): - if not VALID_CODE.match(loaded.entry_name): + if not VALID_CODE_PREFIX.match(loaded.entry_name): raise ExecutionError( f"plugin code for `{loaded.display_name}` does not match " - f"{VALID_CODE.pattern}" + f"{VALID_CODE_PREFIX.pattern}" ) return Plugins( diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index d526fd1..cd5cf4a 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -31,37 +31,6 @@ def _loaded(plugin=None, obj=None, parameters=None): return finder.LoadedPlugin(plugin, obj, parameters) -@pytest.mark.parametrize( - "s", - ( - "E", - "E1", - "E123", - "ABC", - "ABC1", - "ABC123", - ), -) -def test_valid_plugin_prefixes(s): - assert finder.VALID_CODE.match(s) - - -@pytest.mark.parametrize( - "s", - ( - "", - "A1234", - "ABCD", - "abc", - "a-b", - "☃", - "A𝟗", - ), -) -def test_invalid_plugin_prefixes(s): - assert finder.VALID_CODE.match(s) is None - - def test_loaded_plugin_entry_name_vs_display_name(): loaded = _loaded(_plugin(package="package-name", ep=_ep(name="Q"))) assert loaded.entry_name == "Q" diff --git a/tests/unit/test_defaults.py b/tests/unit/test_defaults.py new file mode 100644 index 0000000..822b8f0 --- /dev/null +++ b/tests/unit/test_defaults.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import pytest + +from flake8.defaults import VALID_CODE_PREFIX + + +@pytest.mark.parametrize( + "s", + ( + "E", + "E1", + "E123", + "ABC", + "ABC1", + "ABC123", + ), +) +def test_valid_plugin_prefixes(s): + assert VALID_CODE_PREFIX.match(s) + + +@pytest.mark.parametrize( + "s", + ( + "", + "A1234", + "ABCD", + "abc", + "a-b", + "☃", + "A𝟗", + ), +) +def test_invalid_plugin_prefixes(s): + assert VALID_CODE_PREFIX.match(s) is None diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 8c8f0cb..43d8104 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -220,3 +220,35 @@ def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog): def test_load_config_missing_file_raises_exception(capsys): with pytest.raises(exceptions.ExecutionError): config.load_config("foo.cfg", []) + + +def test_invalid_ignore_codes_raise_error(tmpdir, opt_manager): + tmpdir.join("setup.cfg").write("[flake8]\nignore = E203, //comment") + with tmpdir.as_cwd(): + cfg, _ = config.load_config("setup.cfg", [], isolated=False) + + with pytest.raises(ValueError) as excinfo: + config.parse_config(opt_manager, cfg, tmpdir) + + expected = ( + "Error code '//comment' supplied to 'ignore' option " + "does not match '^[A-Z]{1,3}[0-9]{0,3}$'" + ) + (msg,) = excinfo.value.args + assert msg == expected + + +def test_invalid_extend_ignore_codes_raise_error(tmpdir, opt_manager): + tmpdir.join("setup.cfg").write("[flake8]\nextend-ignore = E203, //comment") + with tmpdir.as_cwd(): + cfg, _ = config.load_config("setup.cfg", [], isolated=False) + + with pytest.raises(ValueError) as excinfo: + config.parse_config(opt_manager, cfg, tmpdir) + + expected = ( + "Error code '//comment' supplied to 'extend-ignore' option " + "does not match '^[A-Z]{1,3}[0-9]{0,3}$'" + ) + (msg,) = excinfo.value.args + assert msg == expected From 314b9f51616cd7d71e2fd8fbf39138ab7a86f075 Mon Sep 17 00:00:00 2001 From: lt94 Date: Sun, 6 Nov 2022 19:29:05 +0800 Subject: [PATCH 158/257] Raise exception if append-config does not exist --- src/flake8/options/config.py | 5 ++++- tests/unit/test_options_config.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index a9ced1b..b51949c 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -87,7 +87,10 @@ def load_config( # TODO: remove this and replace it with configuration modifying plugins # read the additional configs afterwards for filename in extra: - cfg.read(filename, encoding="UTF-8") + if not cfg.read(filename, encoding="UTF-8"): + raise exceptions.ExecutionError( + f"The specified config file does not exist: {filename}" + ) return cfg, cfg_dir diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 43d8104..7de58f0 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -222,6 +222,11 @@ def test_load_config_missing_file_raises_exception(capsys): config.load_config("foo.cfg", []) +def test_load_config_missing_append_config_raise_exception(): + with pytest.raises(exceptions.ExecutionError): + config.load_config(None, ["dont_exist_config.cfg"], isolated=False) + + def test_invalid_ignore_codes_raise_error(tmpdir, opt_manager): tmpdir.join("setup.cfg").write("[flake8]\nignore = E203, //comment") with tmpdir.as_cwd(): From 82953c9ae3d29d8b0208d7a8e44305b338524366 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 01:55:50 +0000 Subject: [PATCH 159/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.0 → v3.2.2](https://github.com/asottile/pyupgrade/compare/v3.2.0...v3.2.2) - [github.com/pre-commit/mirrors-mypy: v0.982 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.982...v0.990) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a154c0..9e602e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.2.2 hooks: - id: pyupgrade args: [--py37-plus] @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v0.990 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 88457a0894411d706b535843b6760734c47ae637 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 15 Nov 2022 18:52:52 -0500 Subject: [PATCH 160/257] remove optparse support --- src/flake8/options/manager.py | 112 ++---------------------------- tests/unit/test_option_manager.py | 91 ------------------------ 2 files changed, 4 insertions(+), 199 deletions(-) diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 8b4ec19..4fd26b2 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -7,7 +7,6 @@ import functools import logging from typing import Any from typing import Callable -from typing import Mapping from typing import Sequence from flake8 import utils @@ -20,55 +19,6 @@ LOG = logging.getLogger(__name__) _ARG = enum.Enum("_ARG", "NO") -_optparse_callable_map: dict[str, type[Any] | _ARG] = { - "int": int, - "long": int, - "string": str, - "float": float, - "complex": complex, - "choice": _ARG.NO, - # optparse allows this but does not document it - "str": str, -} - - -class _CallbackAction(argparse.Action): - """Shim for optparse-style callback actions.""" - - def __init__( - self, - *args: Any, - callback: Callable[..., Any], - callback_args: Sequence[Any] = (), - callback_kwargs: dict[str, Any] | None = None, - **kwargs: Any, - ) -> None: - self._callback = callback - self._callback_args = callback_args - self._callback_kwargs = callback_kwargs or {} - super().__init__(*args, **kwargs) - - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: Sequence[str] | str | None, - option_string: str | None = None, - ) -> None: - if not values: - values = None - elif isinstance(values, list) and len(values) > 1: - values = tuple(values) - self._callback( - self, - option_string, - values, - parser, - *self._callback_args, - **self._callback_kwargs, - ) - - def _flake8_normalize( value: str, *args: str, @@ -95,21 +45,16 @@ class Option: self, short_option_name: str | _ARG = _ARG.NO, long_option_name: str | _ARG = _ARG.NO, - # Options below here are taken from the optparse.Option class + # Options below are taken from argparse.ArgumentParser.add_argument action: str | type[argparse.Action] | _ARG = _ARG.NO, default: Any | _ARG = _ARG.NO, - type: str | Callable[..., Any] | _ARG = _ARG.NO, + type: Callable[..., Any] | _ARG = _ARG.NO, dest: str | _ARG = _ARG.NO, nargs: int | str | _ARG = _ARG.NO, const: Any | _ARG = _ARG.NO, choices: Sequence[Any] | _ARG = _ARG.NO, help: str | _ARG = _ARG.NO, metavar: str | _ARG = _ARG.NO, - # deprecated optparse-only options - callback: Callable[..., Any] | _ARG = _ARG.NO, - callback_args: Sequence[Any] | _ARG = _ARG.NO, - callback_kwargs: Mapping[str, Any] | _ARG = _ARG.NO, - # Options below are taken from argparse.ArgumentParser.add_argument required: bool | _ARG = _ARG.NO, # Options below here are specific to Flake8 parse_from_config: bool = False, @@ -150,21 +95,9 @@ class Option: :param type: A callable to normalize the type (as is the case in - :mod:`argparse`). Deprecated: you can also pass through type - strings such as ``'int'`` which are handled by :mod:`optparse`. + :mod:`argparse`). :param action: - Any action allowed by :mod:`argparse`. Deprecated: this also - understands the ``action='callback'`` action from :mod:`optparse`. - :param callback: - Callback used if the action is ``"callback"``. Deprecated: please - use ``action=`` instead. - :param callback_args: - Additional positional arguments to the callback callable. - Deprecated: please use ``action=`` instead (probably with - ``functools.partial``). - :param callback_kwargs: - Keyword arguments to the callback callable. Deprecated: please - use ``action=`` instead (probably with ``functools.partial``). + Any action allowed by :mod:`argparse`. The following parameters are for Flake8's option handling alone. @@ -184,37 +117,6 @@ class Option: ): short_option_name, long_option_name = _ARG.NO, short_option_name - # optparse -> argparse `%default` => `%(default)s` - if help is not _ARG.NO and "%default" in help: - LOG.warning( - "option %s: please update `help=` text to use %%(default)s " - "instead of %%default -- this will be an error in the future", - long_option_name, - ) - help = help.replace("%default", "%(default)s") - - # optparse -> argparse for `callback` - if action == "callback": - LOG.warning( - "option %s: please update from optparse `action='callback'` " - "to argparse action classes -- this will be an error in the " - "future", - long_option_name, - ) - action = _CallbackAction - if type is _ARG.NO: - nargs = 0 - - # optparse -> argparse for `type` - if isinstance(type, str): - LOG.warning( - "option %s: please update from optparse string `type=` to " - "argparse callable `type=` -- this will be an error in the " - "future", - long_option_name, - ) - type = _optparse_callable_map[type] - # flake8 special type normalization if comma_separated_list or normalize_paths: type = functools.partial( @@ -237,9 +139,6 @@ class Option: self.nargs = nargs self.const = const self.choices = choices - self.callback = callback - self.callback_args = callback_args - self.callback_kwargs = callback_kwargs self.help = help self.metavar = metavar self.required = required @@ -251,9 +150,6 @@ class Option: "nargs": self.nargs, "const": self.const, "choices": self.choices, - "callback": self.callback, - "callback_args": self.callback_args, - "callback_kwargs": self.callback_kwargs, "help": self.help, "metavar": self.metavar, "required": self.required, diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index 3d3ddc1..92266f3 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -3,7 +3,6 @@ from __future__ import annotations import argparse import os -from unittest import mock import pytest @@ -170,96 +169,6 @@ def test_extend_default_ignore(optmanager): assert optmanager.extended_default_ignore == ["T100", "T101", "T102"] -def test_optparse_normalize_callback_option_legacy(optmanager): - """Test the optparse shim for `callback=`.""" - callback_foo = mock.Mock() - optmanager.add_option( - "--foo", - action="callback", - callback=callback_foo, - callback_args=(1, 2), - callback_kwargs={"a": "b"}, - ) - callback_bar = mock.Mock() - optmanager.add_option( - "--bar", - action="callback", - type="string", - callback=callback_bar, - ) - callback_baz = mock.Mock() - optmanager.add_option( - "--baz", - action="callback", - type="string", - nargs=2, - callback=callback_baz, - ) - - optmanager.parse_args(["--foo", "--bar", "bararg", "--baz", "1", "2"]) - - callback_foo.assert_called_once_with( - mock.ANY, # the option / action instance - "--foo", - None, - mock.ANY, # the OptionParser / ArgumentParser - 1, - 2, - a="b", - ) - callback_bar.assert_called_once_with( - mock.ANY, # the option / action instance - "--bar", - "bararg", - mock.ANY, # the OptionParser / ArgumentParser - ) - callback_baz.assert_called_once_with( - mock.ANY, # the option / action instance - "--baz", - ("1", "2"), - mock.ANY, # the OptionParser / ArgumentParser - ) - - -@pytest.mark.parametrize( - ("type_s", "input_val", "expected"), - ( - ("int", "5", 5), - ("long", "6", 6), - ("string", "foo", "foo"), - ("float", "1.5", 1.5), - ("complex", "1+5j", 1 + 5j), - # optparse allows this but does not document it - ("str", "foo", "foo"), - ), -) -def test_optparse_normalize_types(optmanager, type_s, input_val, expected): - """Test the optparse shim for type="typename".""" - optmanager.add_option("--foo", type=type_s) - opts = optmanager.parse_args(["--foo", input_val]) - assert opts.foo == expected - - -def test_optparse_normalize_choice_type(optmanager): - """Test the optparse shim for type="choice".""" - optmanager.add_option("--foo", type="choice", choices=("1", "2", "3")) - opts = optmanager.parse_args(["--foo", "1"]) - assert opts.foo == "1" - # fails to parse - with pytest.raises(SystemExit): - optmanager.parse_args(["--foo", "4"]) - - -def test_optparse_normalize_help(optmanager, capsys): - """Test the optparse shim for %default in help text.""" - optmanager.add_option("--foo", default="bar", help="default: %default") - with pytest.raises(SystemExit): - optmanager.parse_args(["--help"]) - out, err = capsys.readouterr() - output = out + err - assert "default: bar" in output - - @pytest.mark.parametrize( ("s", "is_auto", "n_jobs"), ( From aa002ee4ed7fcb4c6ffdec29b4dc122244638c64 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 18 Nov 2022 11:32:13 -0500 Subject: [PATCH 161/257] require python 3.8.1+ --- .github/workflows/main.yml | 18 +++---- .pre-commit-config.yaml | 4 +- setup.cfg | 8 +-- src/flake8/_compat.py | 18 ------- src/flake8/plugins/finder.py | 14 ++--- tests/integration/test_checker.py | 4 +- tests/integration/test_main.py | 2 - tests/unit/plugins/finder_test.py | 82 ++++++++++++++--------------- tests/unit/plugins/reporter_test.py | 4 +- tests/unit/test_debug.py | 4 +- tests/unit/test_file_checker.py | 4 +- tox.ini | 2 +- 12 files changed, 70 insertions(+), 94 deletions(-) delete mode 100644 src/flake8/_compat.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 868b5ea..e5799e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,10 +13,7 @@ jobs: include: # linux - os: ubuntu-latest - python: pypy-3.7 - toxenv: py - - os: ubuntu-latest - python: 3.7 + python: pypy-3.8 toxenv: py - os: ubuntu-latest python: 3.8 @@ -25,21 +22,24 @@ jobs: python: 3.9 toxenv: py - os: ubuntu-latest - python: '3.10.0-alpha - 3.10.999' + python: '3.10' + toxenv: py + - os: ubuntu-latest + python: '3.11' toxenv: py # windows - os: windows-latest - python: 3.7 + python: 3.8 toxenv: py # misc - os: ubuntu-latest - python: 3.9 + python: '3.10' toxenv: docs - os: ubuntu-latest - python: 3.9 + python: '3.10' toxenv: linters - os: ubuntu-latest - python: 3.9 + python: '3.10' toxenv: dogfood runs-on: ${{ matrix.os }} steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e602e1..10914b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,14 +13,14 @@ repos: - id: reorder-python-imports args: [ --application-directories, '.:src', - --py37-plus, + --py38-plus, --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade rev: v3.2.2 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/psf/black rev: 22.10.0 hooks: diff --git a/setup.cfg b/setup.cfg index 5e20adf..36e90b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,10 +20,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries :: Python Modules @@ -41,8 +37,8 @@ install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.9.0,<2.10.0 pyflakes>=2.5.0,<2.6.0 - importlib-metadata>=1.1.0,<4.3;python_version<"3.8" -python_requires = >=3.7 +# 3.8.0's importlib.metadata is broken +python_requires = >=3.8.1 [options.packages.find] where = src diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py deleted file mode 100644 index 91770bc..0000000 --- a/src/flake8/_compat.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Expose backports in a single place.""" -from __future__ import annotations - -import sys - -if sys.version_info >= (3, 8): # pragma: no cover (PY38+) - import importlib.metadata as importlib_metadata -else: # pragma: no cover ( Generator[Plugin, None, None]: - pyflakes_meta = importlib_metadata.distribution("pyflakes").metadata - pycodestyle_meta = importlib_metadata.distribution("pycodestyle").metadata + pyflakes_meta = importlib.metadata.distribution("pyflakes").metadata + pycodestyle_meta = importlib.metadata.distribution("pycodestyle").metadata for ep in eps: if ep.group not in FLAKE8_GROUPS: @@ -176,7 +176,7 @@ def _flake8_plugins( def _find_importlib_plugins() -> Generator[Plugin, None, None]: # some misconfigured pythons (RHEL) have things on `sys.path` twice seen = set() - for dist in importlib_metadata.distributions(): + for dist in importlib.metadata.distributions(): # assigned to prevent continual reparsing eps = dist.entry_points @@ -221,7 +221,7 @@ def _find_local_plugins( ): name, _, entry_str = plugin_s.partition("=") name, entry_str = name.strip(), entry_str.strip() - ep = importlib_metadata.EntryPoint(name, entry_str, group) + ep = importlib.metadata.EntryPoint(name, entry_str, group) yield Plugin("local", "local", ep) diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 13ec8c1..a585f5a 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -1,13 +1,13 @@ """Integration tests for the checker submodule.""" from __future__ import annotations +import importlib.metadata import sys from unittest import mock import pytest from flake8 import checker -from flake8._compat import importlib_metadata from flake8.plugins import finder from flake8.processor import FileProcessor @@ -85,7 +85,7 @@ def mock_file_checker_with_plugin(plugin_target): finder.Plugin( "flake-package", "9001", - importlib_metadata.EntryPoint( + importlib.metadata.EntryPoint( "Q", f"{plugin_target.__module__}:{plugin_target.__name__}", "flake8.extension", diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 96d1182..68b93cb 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -170,8 +170,6 @@ def test_tokenization_error_but_not_syntax_error(tmpdir, capsys): if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) expected = "t.py:2:1: E999 SyntaxError: end of file (EOF) in multi-line statement\n" # noqa: E501 - elif sys.version_info < (3, 8): # pragma: no cover ( Date: Tue, 22 Nov 2022 01:20:39 +0000 Subject: [PATCH 162/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10914b7..13c4a0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.990 + rev: v0.991 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 047e6f8eb5111674b4dca4bf692651b716152ef5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Nov 2022 13:31:07 -0500 Subject: [PATCH 163/257] upgrade pycodestyle to 2.10 --- setup.cfg | 2 +- src/flake8/plugins/pycodestyle.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/setup.cfg b/setup.cfg index 36e90b4..d2959c8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ package_dir = # https://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.9.0,<2.10.0 + pycodestyle>=2.10.0,<2.11.0 pyflakes>=2.5.0,<2.6.0 # 3.8.0's importlib.metadata is broken python_requires = >=3.8.1 diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py index 4b0d67f..7eb90e1 100644 --- a/src/flake8/plugins/pycodestyle.py +++ b/src/flake8/plugins/pycodestyle.py @@ -26,11 +26,7 @@ from pycodestyle import missing_whitespace_after_keyword as _missing_whitespace_ from pycodestyle import missing_whitespace_around_operator as _missing_whitespace_around_operator # noqa: E501 from pycodestyle import module_imports_on_top_of_file as _module_imports_on_top_of_file # noqa: E501 from pycodestyle import python_3000_async_await_keywords as _python_3000_async_await_keywords # noqa: E501 -from pycodestyle import python_3000_backticks as _python_3000_backticks -from pycodestyle import python_3000_has_key as _python_3000_has_key from pycodestyle import python_3000_invalid_escape_sequence as _python_3000_invalid_escape_sequence # noqa: E501 -from pycodestyle import python_3000_not_equal as _python_3000_not_equal -from pycodestyle import python_3000_raise_comma as _python_3000_raise_comma from pycodestyle import tabs_obsolete as _tabs_obsolete from pycodestyle import tabs_or_spaces as _tabs_or_spaces from pycodestyle import trailing_blank_lines as _trailing_blank_lines @@ -83,11 +79,7 @@ def pycodestyle_logical( yield from _missing_whitespace_around_operator(logical_line, tokens) yield from _module_imports_on_top_of_file(logical_line, indent_level, checker_state, noqa) # noqa: E501 yield from _python_3000_async_await_keywords(logical_line, tokens) - yield from _python_3000_backticks(logical_line) - yield from _python_3000_has_key(logical_line, noqa) yield from _python_3000_invalid_escape_sequence(logical_line, tokens, noqa) - yield from _python_3000_not_equal(logical_line) - yield from _python_3000_raise_comma(logical_line) yield from _whitespace_around_comma(logical_line) yield from _whitespace_around_keywords(logical_line) yield from _whitespace_around_named_parameter_equals(logical_line, tokens) From 489be4d30a37273e67683d14758e3a7912d46571 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Nov 2022 13:56:50 -0500 Subject: [PATCH 164/257] upgrade pyflakes to 3.0.0 --- setup.cfg | 2 +- src/flake8/plugins/pyflakes.py | 17 +++-------------- tests/unit/plugins/finder_test.py | 2 +- tests/unit/test_pyflakes_codes.py | 2 +- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/setup.cfg b/setup.cfg index d2959c8..6c28b90 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ package_dir = install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.10.0,<2.11.0 - pyflakes>=2.5.0,<2.6.0 + pyflakes>=3.0.0,<3.1.0 # 3.8.0's importlib.metadata is broken python_requires = >=3.8.1 diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index cffea36..a9dbc4c 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -4,7 +4,6 @@ from __future__ import annotations import argparse import ast import os -import tokenize from typing import Any from typing import Generator @@ -52,13 +51,13 @@ FLAKE8_PYFLAKES_CODES = { "DefaultExceptNotLast": "F707", "DoctestSyntaxError": "F721", "ForwardAnnotationSyntaxError": "F722", - "CommentAnnotationSyntaxError": "F723", "RedefinedWhileUnused": "F811", "UndefinedName": "F821", "UndefinedExport": "F822", "UndefinedLocal": "F823", "DuplicateArgument": "F831", "UnusedVariable": "F841", + "UnusedAnnotation": "F842", "RaiseNotImplemented": "F901", } @@ -70,12 +69,7 @@ class FlakesChecker(pyflakes.checker.Checker): include_in_doctest: list[str] = [] exclude_from_doctest: list[str] = [] - def __init__( - self, - tree: ast.AST, - file_tokens: list[tokenize.TokenInfo], - filename: str, - ) -> None: + def __init__(self, tree: ast.AST, filename: str) -> None: """Initialize the PyFlakes plugin with an AST tree and filename.""" filename = utils.normalize_path(filename) with_doctest = self.with_doctest @@ -99,12 +93,7 @@ class FlakesChecker(pyflakes.checker.Checker): if overlapped_by: with_doctest = True - super().__init__( - tree, - filename=filename, - withDoctest=with_doctest, - file_tokens=file_tokens, - ) + super().__init__(tree, filename=filename, withDoctest=with_doctest) @classmethod def add_options(cls, parser: OptionManager) -> None: diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index 89de0a7..b289bef 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -685,7 +685,7 @@ def test_load_plugin_ok(): assert loaded == finder.LoadedPlugin( plugin, FlakesChecker, - {"tree": True, "file_tokens": True, "filename": True}, + {"tree": True, "filename": True}, ) diff --git a/tests/unit/test_pyflakes_codes.py b/tests/unit/test_pyflakes_codes.py index 4626e3d..444008a 100644 --- a/tests/unit/test_pyflakes_codes.py +++ b/tests/unit/test_pyflakes_codes.py @@ -27,7 +27,7 @@ def f(): sys = sys """ tree = ast.parse(src) - checker = pyflakes_shim.FlakesChecker(tree, [], "t.py") + checker = pyflakes_shim.FlakesChecker(tree, "t.py") message_texts = [s for _, _, s, _ in checker.run()] assert message_texts == [ "F823 local variable 'sys' defined in enclosing scope on line 1 referenced before assignment", # noqa: E501 From b9a7794c4f425ef8419081e6993f99419cc667ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Nov 2022 14:27:13 -0500 Subject: [PATCH 165/257] Release 6.0.0 --- docs/source/internal/releases.rst | 4 ++-- docs/source/release-notes/6.0.0.rst | 35 +++++++++++++++++++++++++++++ docs/source/release-notes/index.rst | 6 +++++ src/flake8/__init__.py | 2 +- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 docs/source/release-notes/6.0.0.rst diff --git a/docs/source/internal/releases.rst b/docs/source/internal/releases.rst index f15ea0c..39fbdf7 100644 --- a/docs/source/internal/releases.rst +++ b/docs/source/internal/releases.rst @@ -28,9 +28,9 @@ Historically, |Flake8| has generated major releases for: - Unvendoring dependencies (2.0) -- Large scale refactoring (2.0, 3.0, 5.0) +- Large scale refactoring (2.0, 3.0, 5.0, 6.0) -- Subtly breaking CLI changes (3.0, 4.0, 5.0) +- Subtly breaking CLI changes (3.0, 4.0, 5.0, 6.0) - Breaking changes to its plugin interface (3.0) diff --git a/docs/source/release-notes/6.0.0.rst b/docs/source/release-notes/6.0.0.rst new file mode 100644 index 0000000..edb01df --- /dev/null +++ b/docs/source/release-notes/6.0.0.rst @@ -0,0 +1,35 @@ +6.0.0 -- 2022-11-23 +------------------- + +You can view the `6.0.0 milestone`_ on GitHub for more details. + +Backwards Incompatible Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove ``--diff`` option (See also :issue:`1389`, :pull:`1720`). +- Produce an error when invalid codes are specified in configuration (See also + :issue:`1689`, :pull:`1713`). +- Produce an error if the file specified in ``--extend-config`` does not exist + (See also :issue:`1729`, :pull:`1732`). +- Remove ``optparse`` compatibility support (See also :pull:`1739`). + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.10.0, < 2.11.0 (See also :pull:`1746`). +- Pyflakes has been updated to >= 3.0.0, < 3.1.0 (See also :pull:`1748`). + +Features +~~~~~~~~ + +- Require python >= 3.8.1 (See also :pull:`1633`, :pull:`1741`). +- List available formatters in for ``--format`` option in ``--help`` (See also + :issue:`223`, :pull:`1624`). +- Improve multiprocessing performance (See also :pull:`1723`). +- Enable multiprocessing on non-fork platforms (See also :pull:`1723`). +- Ensure results are sorted when discovered from files (See also :issue:`1670`, + :pull:`1726`). + +.. all links +.. _6.0.0 milestone: + https://github.com/PyCQA/flake8/milestone/47 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index d511f30..a9eff5f 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -5,6 +5,12 @@ All of the release notes that have been recorded for Flake8 are organized here with the newest releases first. +6.x Release Series +================== + +.. toctree:: + 6.0.0 + 5.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 43f3bec..c6d48b7 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "5.0.4" +__version__ = "6.0.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From cf6d3fe0c6faf7a8a3835ba97edac3238c8b1edc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 24 Nov 2022 16:28:16 -0500 Subject: [PATCH 166/257] remove MANIFEST.in packagers: please package from the github tgz --- MANIFEST.in | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 74b090b..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -include *.rst -include CONTRIBUTORS.txt -include LICENSE -include *.ini -global-exclude *.pyc -recursive-include docs *.rst *.py -recursive-include tests *.py *.ini *.rst *_diff -recursive-include src *.py -prune docs/build/ From 5fde644640496af61b580193178e34c8aafefc86 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Nov 2022 04:04:15 +0000 Subject: [PATCH 167/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) - [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13c4a0f..ad9c62b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-yaml - id: debug-statements @@ -27,7 +27,7 @@ repos: - id: black args: [--line-length=79] - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From c0d0ced5b83ca5f72d438ea86d0cc0fdf2dd8345 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 02:21:54 +0000 Subject: [PATCH 168/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.2 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.2...v3.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad9c62b..f2ca587 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.0 hooks: - id: pyupgrade args: [--py38-plus] From 00fbfd7774aca3d3fa36e5b3c8e1ac6080edf88c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 03:27:50 +0000 Subject: [PATCH 169/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.3.0...v3.3.1) - [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2ca587..1796476 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,12 +17,12 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.0 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black args: [--line-length=79] From 837e81948bdf81da1193584dfbb98ca16a2dc997 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 14 Dec 2022 12:55:37 -0500 Subject: [PATCH 170/257] communicate that --select is usually unnecessary --- docs/source/user/options.rst | 8 ++++++-- src/flake8/defaults.py | 1 - src/flake8/main/options.py | 14 +++++++++----- src/flake8/style_guide.py | 2 +- tests/unit/test_decision_engine.py | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index f9e8b86..8db2df8 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -607,13 +607,14 @@ Options and their Descriptions :ref:`Go back to index ` + **You usually do not need to specify this option as the default includes + all installed plugin codes.** + Specify the list of error codes you wish |Flake8| to report. Similarly to :option:`--ignore`. You can specify a portion of an error code to get all that start with that string. For example, you can use ``E``, ``E4``, ``E43``, and ``E431``. - This defaults to: ``E,F,W,C90`` - Command-line example: .. prompt:: bash @@ -649,6 +650,9 @@ Options and their Descriptions .. versionadded:: 4.0.0 + **You usually do not need to specify this option as the default includes + all installed plugin codes.** + Specify a list of codes to add to the list of selected ones. Similar considerations as in :option:`--select` apply here with regard to the value. diff --git a/src/flake8/defaults.py b/src/flake8/defaults.py index e3edf32..8add695 100644 --- a/src/flake8/defaults.py +++ b/src/flake8/defaults.py @@ -16,7 +16,6 @@ EXCLUDE = ( "*.egg", ) IGNORE = ("E121", "E123", "E126", "E226", "E24", "E704", "W503", "W504") -SELECT = ("E", "F", "W", "C90") MAX_LINE_LENGTH = 79 INDENT_SIZE = 4 diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 891bae9..9d57321 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -297,9 +297,11 @@ def register_default_options(option_manager: OptionManager) -> None: parse_from_config=True, comma_separated_list=True, help=( - f"Comma-separated list of error codes to enable. " - f"For example, ``--select=E4,E51,W234``. " - f"(Default: {','.join(defaults.SELECT)})" + "Limit the reported error codes to codes prefix-matched by this " + "list. " + "You usually do not need to specify this option as the default " + "includes all installed plugin codes. " + "For example, ``--select=E4,E51,W234``." ), ) @@ -309,8 +311,10 @@ def register_default_options(option_manager: OptionManager) -> None: parse_from_config=True, comma_separated_list=True, help=( - "Comma-separated list of error codes to add to the list " - "of selected ones. For example, ``--extend-select=E4,E51,W234``." + "Add additional error codes to the default ``--select``. " + "You usually do not need to specify this option as the default " + "includes all installed plugin codes. " + "For example, ``--extend-select=E4,E51,W234``." ), ) diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index 8431650..a409484 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -88,7 +88,7 @@ class DecisionEngine: self.selected = _select_ignore( option=options.select, - default=defaults.SELECT, + default=(), extended_default=options.extended_default_select, extend=options.extend_select, ) diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index 27ced3f..d543d5e 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -14,7 +14,7 @@ def create_options(**kwargs): kwargs.setdefault("ignore", None) kwargs.setdefault("extend_select", None) kwargs.setdefault("extend_ignore", None) - kwargs.setdefault("extended_default_select", []) + kwargs.setdefault("extended_default_select", ["C90", "F", "E", "W"]) kwargs.setdefault("extended_default_ignore", []) kwargs.setdefault("disable_noqa", False) return argparse.Namespace(**kwargs) From 4778fe9643e94123f93a306f1ed63b52323eee35 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 14 Dec 2022 13:25:35 -0500 Subject: [PATCH 171/257] deprecate --include-in-doctest --exclude-from-doctest --- src/flake8/plugins/pyflakes.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index a9dbc4c..f165c95 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import ast +import logging import os from typing import Any from typing import Generator @@ -12,6 +13,8 @@ import pyflakes.checker from flake8 import utils from flake8.options.manager import OptionManager +LOG = logging.getLogger(__name__) + FLAKE8_PYFLAKES_CODES = { "UnusedImport": "F401", "ImportShadowedByLoopVar": "F402", @@ -137,6 +140,12 @@ class FlakesChecker(pyflakes.checker.Checker): cls.builtIns = cls.builtIns.union(options.builtins) cls.with_doctest = options.doctests + if options.include_in_doctest or options.exclude_from_doctest: + LOG.warning( + "--include-in-doctest / --exclude-from-doctest will be " + "removed in a future version. see PyCQA/flake8#1747" + ) + included_files = [] for included_file in options.include_in_doctest: if included_file == "": From 61138f1f4b00ae9e00b3262c566de9b2afdc5584 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 10:25:13 -0500 Subject: [PATCH 172/257] correct tidelift link --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 7267456..1ac2512 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ github: asottile -tidelift: pypi/pre-commit +tidelift: pypi/flake8 From bc6e08723541125b0340bd6b6f72fdd3e30e9ca9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 07:41:47 -0800 Subject: [PATCH 173/257] use pypy3.9 to work around coverage issue --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5799e1..7f28cf9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: include: # linux - os: ubuntu-latest - python: pypy-3.8 + python: pypy-3.9 toxenv: py - os: ubuntu-latest python: 3.8 From 0be0ae784269a96c3022c47ead652ec93e07bf63 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 11 Jan 2023 09:19:54 -0500 Subject: [PATCH 174/257] add myself to maintenance --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f2a6d43..1ea6096 100644 --- a/README.rst +++ b/README.rst @@ -85,5 +85,6 @@ Links Maintenance =========== -Flake8 was created by Tarek Ziadé and is currently maintained by `Ian Cordasco +Flake8 was created by Tarek Ziadé and is currently maintained by `anthony sottile +`_ and `Ian Cordasco `_ From e6f9db5039a79d0776e416314e1e331c611d7319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sat, 21 Jan 2023 21:37:35 +0100 Subject: [PATCH 175/257] Update python-api.rst Fix minor typo in doc --- docs/source/user/python-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/user/python-api.rst b/docs/source/user/python-api.rst index ed7e5e5..f51b146 100644 --- a/docs/source/user/python-api.rst +++ b/docs/source/user/python-api.rst @@ -70,7 +70,7 @@ namely .. warning:: These are not *perfectly* backwards compatible. Not all arguments are - respsected, and some of the types necessary for something to work have + respected, and some of the types necessary for something to work have changed. Most people, we observed, were using From 50755610d2161e8d6b15d37ca3c9a651eca7a443 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 07:21:58 +0000 Subject: [PATCH 176/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1796476..4fa1718 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: [--line-length=79] From b0d7912f7e4232cbaa87d9ba26df3cbd583b0ea0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 07:22:49 +0000 Subject: [PATCH 177/257] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/flake8/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 2cae545..8d70034 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -160,7 +160,7 @@ class Manager: def _handle_results(self, filename: str, results: Results) -> int: style_guide = self.style_guide reported_results_count = 0 - for (error_code, line_number, column, text, physical_line) in results: + for error_code, line_number, column, text, physical_line in results: reported_results_count += style_guide.handle_error( code=error_code, filename=filename, @@ -423,7 +423,7 @@ class FileChecker: runner = checker.run() except AttributeError: runner = checker - for (line_number, offset, text, _) in runner: + for line_number, offset, text, _ in runner: self.report( error_code=None, line_number=line_number, From 77e075c66a4f2642d1168da307f6fe42c5348ba8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 05:36:21 +0000 Subject: [PATCH 178/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.0](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fa1718..6387b06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.0.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 2615d21a5bb19d7c6df0172cb3f536cf4c2fec72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 05:18:56 +0000 Subject: [PATCH 179/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.0 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.0...v1.0.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6387b06..123eb87 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.0 + rev: v1.0.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From f096f853c26bc8db75a0c096251122d27262f099 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Mar 2023 06:44:36 +0000 Subject: [PATCH 180/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 123eb87..059562b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.1 + rev: v1.1.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 050d0dcf4d9c4ff1b1083f02eb946eca6eab5988 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 09:38:50 +0000 Subject: [PATCH 181/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 059562b..f0303bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black args: [--line-length=79] From 426f952fc89d9cf9b8f0ad9fa5fd4d28ef96b13c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 06:25:16 +0000 Subject: [PATCH 182/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.1.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.1.1...v1.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0303bd..e2b95f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.1.1 + rev: v1.2.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From cabc45abab39e136290f1c93ab36177cf2151eba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 07:12:41 +0000 Subject: [PATCH 183/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2b95f5..5994d20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py38-plus] From 0c3cf066d301a140eb87d3b0681d32c60f0139a5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 30 Apr 2023 15:03:01 -0400 Subject: [PATCH 184/257] remove regex link --- src/flake8/defaults.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flake8/defaults.py b/src/flake8/defaults.py index 8add695..57abda1 100644 --- a/src/flake8/defaults.py +++ b/src/flake8/defaults.py @@ -36,7 +36,6 @@ NOQA_INLINE_REGEXP = re.compile( # We do not want to capture the ``: `` that follows ``noqa`` # We do not care about the casing of ``noqa`` # We want a comma-separated list of errors - # https://regex101.com/r/4XUuax/2 full explanation of the regex r"# noqa(?::[\s]?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?", re.IGNORECASE, ) From a466e461e5cb45b0c0d8c41a75c77f9bbf29786b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 08:00:29 +0000 Subject: [PATCH 185/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - https://github.com/asottile/reorder_python_imports → https://github.com/asottile/reorder-python-imports - [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5994d20..2214c16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace exclude: ^tests/fixtures/ -- repo: https://github.com/asottile/reorder_python_imports +- repo: https://github.com/asottile/reorder-python-imports rev: v3.9.0 hooks: - id: reorder-python-imports @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.4.0 hooks: - id: pyupgrade args: [--py38-plus] From 4b7ede13fb64288ccc3a361a8fd4560c955cf13a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 May 2023 07:06:03 +0000 Subject: [PATCH 186/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2214c16..7047121 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 + rev: v1.3.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 9b6887762bbde293f1e0807fafd997d6213775ff Mon Sep 17 00:00:00 2001 From: Abdulfatai Aka <32123313+abdulfataiaka@users.noreply.github.com> Date: Tue, 16 May 2023 14:44:14 +0100 Subject: [PATCH 187/257] Update invocation.rst file --- docs/source/user/invocation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/user/invocation.rst b/docs/source/user/invocation.rst index 0049ec9..61cef97 100644 --- a/docs/source/user/invocation.rst +++ b/docs/source/user/invocation.rst @@ -51,7 +51,7 @@ Or This is the last time we will show both versions of an invocation. From now on, we'll simply use ``flake8`` and assume that the user - knows they can instead use ``python -m flake8`` instead. + knows they can instead use ``python -m flake8``. It's also possible to narrow what |Flake8| will try to check by specifying exactly the paths and directories you want it to check. Let's assume that From 43266a2e26ec49a55b866c78b295deaebb1debf7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Jun 2023 22:02:26 -0400 Subject: [PATCH 188/257] mute FSTRING_MIDDLE tokens --- src/flake8/processor.py | 8 +++++++- tests/integration/test_plugins.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 16fa16b..c9c9e5d 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -5,6 +5,7 @@ import argparse import ast import contextlib import logging +import sys import tokenize from typing import Any from typing import Generator @@ -178,7 +179,7 @@ class FileProcessor: self.blank_lines = 0 self.tokens = [] - def build_logical_line_tokens(self) -> _Logical: + def build_logical_line_tokens(self) -> _Logical: # noqa: C901 """Build the mapping, comments, and logical line lists.""" logical = [] comments = [] @@ -195,6 +196,11 @@ class FileProcessor: continue if token_type == tokenize.STRING: text = mutate_string(text) + elif ( + sys.version_info >= (3, 12) + and token_type == tokenize.FSTRING_MIDDLE + ): + text = "x" * len(text) if previous_row: (start_row, start_column) = start if previous_row != start_row: diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index edba048..b67e6d6 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -197,3 +197,35 @@ t.py:3:1: T001 '"""\\n' ''' out, err = capsys.readouterr() assert out == expected + + +def yields_logical_line(logical_line): + yield 0, f"T001 {logical_line!r}" + + +def test_logical_line_plugin(tmpdir, capsys): + cfg_s = f"""\ +[flake8] +extend-ignore = F +[flake8:local-plugins] +extension = + T = {yields_logical_line.__module__}:{yields_logical_line.__name__} +""" + + cfg = tmpdir.join("tox.ini") + cfg.write(cfg_s) + + src = """\ +f'hello world' +""" + t_py = tmpdir.join("t.py") + t_py.write_binary(src.encode()) + + with tmpdir.as_cwd(): + assert main(("t.py", "--config", str(cfg))) == 1 + + expected = """\ +t.py:1:1: T001 "f'xxxxxxxxxxx'" +""" + out, err = capsys.readouterr() + assert out == expected From 4d0c97d7afd3c76cbf99f027896c3e93f963136a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 08:00:37 +0000 Subject: [PATCH 189/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.4.0 → v3.6.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7047121..75b58c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.6.0 hooks: - id: pyupgrade args: [--py38-plus] From ad03ede045ce35a8390f4d41b62992de381d28e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 08:45:09 +0000 Subject: [PATCH 190/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.9.0 → v3.10.0](https://github.com/asottile/reorder-python-imports/compare/v3.9.0...v3.10.0) - [github.com/asottile/pyupgrade: v3.6.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.6.0...v3.7.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75b58c0..f15bf88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/reorder-python-imports - rev: v3.9.0 + rev: v3.10.0 hooks: - id: reorder-python-imports args: [ @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.6.0 + rev: v3.7.0 hooks: - id: pyupgrade args: [--py38-plus] From 24999ec87e6e8cd5ddaa815f575467f6ed8bb33e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 07:48:29 +0000 Subject: [PATCH 191/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.4.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.4.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f15bf88..6d2b716 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.4.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From fa42096dfe62fc6d4059b70042e27e4e59353984 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Jul 2023 09:39:09 +0000 Subject: [PATCH 192/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.7.0 → v3.8.0](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.8.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d2b716..d8c9662 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 + rev: v3.8.0 hooks: - id: pyupgrade args: [--py38-plus] From 73825ae81c9e0085efc7d10cdec7eb2917d81b9e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 10:15:18 +0000 Subject: [PATCH 193/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.8.0 → v3.9.0](https://github.com/asottile/pyupgrade/compare/v3.8.0...v3.9.0) - [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8c9662..dbe6918 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,12 +17,12 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.9.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black args: [--line-length=79] From 5bd63bc5528cf0c89ed93c353e65b1089836c105 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 13:03:38 -0400 Subject: [PATCH 194/257] upgrade pyflakes to 3.1.x --- docs/source/user/error-codes.rst | 2 -- setup.cfg | 2 +- src/flake8/plugins/pyflakes.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/source/user/error-codes.rst b/docs/source/user/error-codes.rst index d12a07f..2a91413 100644 --- a/docs/source/user/error-codes.rst +++ b/docs/source/user/error-codes.rst @@ -81,8 +81,6 @@ generates its own :term:`error code`\ s for ``pyflakes``: +------+---------------------------------------------------------------------+ | F702 | a ``continue`` statement outside of a ``while`` or ``for`` loop | +------+---------------------------------------------------------------------+ -| F703 | a ``continue`` statement in a ``finally`` block in a loop | -+------+---------------------------------------------------------------------+ | F704 | a ``yield`` or ``yield from`` statement outside of a function | +------+---------------------------------------------------------------------+ | F706 | a ``return`` statement outside of a function/method | diff --git a/setup.cfg b/setup.cfg index 6c28b90..15dcfeb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ package_dir = install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.10.0,<2.11.0 - pyflakes>=3.0.0,<3.1.0 + pyflakes>=3.1.0,<3.2.0 # 3.8.0's importlib.metadata is broken python_requires = >=3.8.1 diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index f165c95..f62527e 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -48,7 +48,6 @@ FLAKE8_PYFLAKES_CODES = { "IfTuple": "F634", "BreakOutsideLoop": "F701", "ContinueOutsideLoop": "F702", - "ContinueInFinally": "F703", "YieldOutsideFunction": "F704", "ReturnOutsideFunction": "F706", "DefaultExceptNotLast": "F707", From 9786562feb573d30c73f18e1a0a6685c8584e9b5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 13:23:01 -0400 Subject: [PATCH 195/257] upgrade to pycodestyle 2.11.x --- setup.cfg | 2 +- src/flake8/plugins/pycodestyle.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 15dcfeb..593c2e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ package_dir = # https://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.10.0,<2.11.0 + pycodestyle>=2.11.0,<2.12.0 pyflakes>=3.1.0,<3.2.0 # 3.8.0's importlib.metadata is broken python_requires = >=3.8.1 diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py index 7eb90e1..9e1d2bb 100644 --- a/src/flake8/plugins/pycodestyle.py +++ b/src/flake8/plugins/pycodestyle.py @@ -23,9 +23,7 @@ from pycodestyle import maximum_doc_length as _maximum_doc_length from pycodestyle import maximum_line_length as _maximum_line_length from pycodestyle import missing_whitespace as _missing_whitespace from pycodestyle import missing_whitespace_after_keyword as _missing_whitespace_after_keyword # noqa: E501 -from pycodestyle import missing_whitespace_around_operator as _missing_whitespace_around_operator # noqa: E501 from pycodestyle import module_imports_on_top_of_file as _module_imports_on_top_of_file # noqa: E501 -from pycodestyle import python_3000_async_await_keywords as _python_3000_async_await_keywords # noqa: E501 from pycodestyle import python_3000_invalid_escape_sequence as _python_3000_invalid_escape_sequence # noqa: E501 from pycodestyle import tabs_obsolete as _tabs_obsolete from pycodestyle import tabs_or_spaces as _tabs_or_spaces @@ -74,11 +72,9 @@ def pycodestyle_logical( yield from _imports_on_separate_lines(logical_line) yield from _indentation(logical_line, previous_logical, indent_char, indent_level, previous_indent_level, indent_size) # noqa: E501 yield from _maximum_doc_length(logical_line, max_doc_length, noqa, tokens) - yield from _missing_whitespace(logical_line) + yield from _missing_whitespace(logical_line, tokens) yield from _missing_whitespace_after_keyword(logical_line, tokens) - yield from _missing_whitespace_around_operator(logical_line, tokens) yield from _module_imports_on_top_of_file(logical_line, indent_level, checker_state, noqa) # noqa: E501 - yield from _python_3000_async_await_keywords(logical_line, tokens) yield from _python_3000_invalid_escape_sequence(logical_line, tokens, noqa) yield from _whitespace_around_comma(logical_line) yield from _whitespace_around_keywords(logical_line) From acca35b8006e2760ceed58c1fbec34fbac024ff2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 14:30:55 -0400 Subject: [PATCH 196/257] add 3.12 to ci matrix --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f28cf9..e210204 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,9 @@ jobs: - os: ubuntu-latest python: '3.11' toxenv: py + - os: ubuntu-latest + python: '3.12-dev' + toxenv: py # windows - os: windows-latest python: 3.8 From 1ed78d592a355ece4edfee2884185dccaa732dbc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 14:25:54 -0400 Subject: [PATCH 197/257] handle multiline fstrings in 3.12 --- src/flake8/_compat.py | 11 +++++++ src/flake8/checker.py | 15 +++++---- src/flake8/processor.py | 53 +++++++++++++++---------------- tests/integration/test_plugins.py | 32 +++++++++++++++++++ tests/unit/test_file_processor.py | 25 ++++----------- 5 files changed, 83 insertions(+), 53 deletions(-) create mode 100644 src/flake8/_compat.py diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py new file mode 100644 index 0000000..f4a0903 --- /dev/null +++ b/src/flake8/_compat.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import sys +import tokenize + +if sys.version_info >= (3, 12): + FSTRING_START = tokenize.FSTRING_START + FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE + FSTRING_END = tokenize.FSTRING_END +else: + FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1 diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 8d70034..6c4caef 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -20,6 +20,7 @@ from flake8 import defaults from flake8 import exceptions from flake8 import processor from flake8 import utils +from flake8._compat import FSTRING_START from flake8.discover_files import expand_paths from flake8.options.parse_args import parse_args from flake8.plugins.finder import Checkers @@ -551,15 +552,17 @@ class FileChecker: ) -> None: """Run physical checks if and only if it is at the end of the line.""" assert self.processor is not None + if token.type == FSTRING_START: # pragma: >=3.12 cover + self.processor.fstring_start(token.start[0]) # a newline token ends a single physical line. - if processor.is_eol_token(token): + elif processor.is_eol_token(token): # if the file does not end with a newline, the NEWLINE # token is inserted by the parser, but it does not contain # the previous physical line in `token[4]` - if token[4] == "": + if token.line == "": self.run_physical_checks(prev_physical) else: - self.run_physical_checks(token[4]) + self.run_physical_checks(token.line) elif processor.is_multiline_string(token): # Less obviously, a string that contains newlines is a # multiline string, either triple-quoted or with internal @@ -572,10 +575,8 @@ class FileChecker: # - have to wind self.line_number back because initially it # points to the last line of the string, and we want # check_physical() to give accurate feedback - line_no = token[2][0] - with self.processor.inside_multiline(line_number=line_no): - for line in self.processor.split_line(token): - self.run_physical_checks(line) + for line in self.processor.multiline_string(token): + self.run_physical_checks(line) def _try_initialize_processpool( diff --git a/src/flake8/processor.py b/src/flake8/processor.py index c9c9e5d..2eea88f 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -3,9 +3,7 @@ from __future__ import annotations import argparse import ast -import contextlib import logging -import sys import tokenize from typing import Any from typing import Generator @@ -14,6 +12,8 @@ from typing import Tuple from flake8 import defaults from flake8 import utils +from flake8._compat import FSTRING_END +from flake8._compat import FSTRING_MIDDLE from flake8.plugins.finder import LoadedPlugin LOG = logging.getLogger(__name__) @@ -117,6 +117,7 @@ class FileProcessor: self._file_tokens: list[tokenize.TokenInfo] | None = None # map from line number to the line we'll search for `noqa` in self._noqa_line_mapping: dict[int, str] | None = None + self._fstring_start = -1 @property def file_tokens(self) -> list[tokenize.TokenInfo]: @@ -129,14 +130,26 @@ class FileProcessor: return self._file_tokens - @contextlib.contextmanager - def inside_multiline( - self, line_number: int - ) -> Generator[None, None, None]: - """Context-manager to toggle the multiline attribute.""" - self.line_number = line_number + def fstring_start(self, lineno: int) -> None: + """Signal the beginning of an fstring.""" + self._fstring_start = lineno + + def multiline_string( + self, token: tokenize.TokenInfo + ) -> Generator[str, None, None]: + """Iterate through the lines of a multiline string.""" + if token.type == FSTRING_END: + start = self._fstring_start + else: + start = token.start[0] + self.multiline = True - yield + self.line_number = start + # intentionally don't include the last line, that line will be + # terminated later by a future end-of-line + for _ in range(start, token.end[0]): + yield self.lines[self.line_number - 1] + self.line_number += 1 self.multiline = False def reset_blank_before(self) -> None: @@ -196,10 +209,7 @@ class FileProcessor: continue if token_type == tokenize.STRING: text = mutate_string(text) - elif ( - sys.version_info >= (3, 12) - and token_type == tokenize.FSTRING_MIDDLE - ): + elif token_type == FSTRING_MIDDLE: text = "x" * len(text) if previous_row: (start_row, start_column) = start @@ -231,19 +241,6 @@ class FileProcessor: self.statistics["logical lines"] += 1 return joined_comments, self.logical_line, mapping_list - def split_line( - self, token: tokenize.TokenInfo - ) -> Generator[str, None, None]: - """Split a physical line's line based on new-lines. - - This also auto-increments the line number for the caller. - """ - # intentionally don't include the last line, that line will be - # terminated later by a future end-of-line - for line_no in range(token.start[0], token.end[0]): - yield self.lines[line_no - 1] - self.line_number += 1 - def keyword_arguments_for( self, parameters: dict[str, bool], @@ -398,7 +395,9 @@ def is_eol_token(token: tokenize.TokenInfo) -> bool: def is_multiline_string(token: tokenize.TokenInfo) -> bool: """Check if this is a multiline string.""" - return token[0] == tokenize.STRING and "\n" in token[1] + return token.type == FSTRING_END or ( + token.type == tokenize.STRING and "\n" in token.string + ) def token_is_newline(token: tokenize.TokenInfo) -> bool: diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index b67e6d6..d4c22b0 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -199,6 +199,38 @@ t.py:3:1: T001 '"""\\n' assert out == expected +def test_physical_line_plugin_multiline_fstring(tmpdir, capsys): + cfg_s = f"""\ +[flake8:local-plugins] +extension = + T = {yields_physical_line.__module__}:{yields_physical_line.__name__} +""" + + cfg = tmpdir.join("tox.ini") + cfg.write(cfg_s) + + src = '''\ +y = 1 +x = f""" +hello {y} +""" +''' + t_py = tmpdir.join("t.py") + t_py.write_binary(src.encode()) + + with tmpdir.as_cwd(): + assert main(("t.py", "--config", str(cfg))) == 1 + + expected = '''\ +t.py:1:1: T001 'y = 1\\n' +t.py:2:1: T001 'x = f"""\\n' +t.py:3:1: T001 'hello {y}\\n' +t.py:4:1: T001 '"""\\n' +''' + out, err = capsys.readouterr() + assert out == expected + + def yields_logical_line(logical_line): yield 0, f"T001 {logical_line!r}" diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index bd693b3..a90c628 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -275,13 +275,15 @@ def test_processor_split_line(default_options): (3, 3), 'x = """\ncontents\n"""\n', ) - expected = [('x = """\n', 0), ("contents\n", 1)] + expected = [('x = """\n', 1, True), ("contents\n", 2, True)] + assert file_processor.multiline is False actual = [ - (line, file_processor.line_number) - for line in file_processor.split_line(token) + (line, file_processor.line_number, file_processor.multiline) + for line in file_processor.multiline_string(token) ] + assert file_processor.multiline is False assert expected == actual - assert file_processor.line_number == 2 + assert file_processor.line_number == 3 def test_build_ast(default_options): @@ -321,21 +323,6 @@ def test_visited_new_blank_line(default_options): assert file_processor.blank_lines == 1 -def test_inside_multiline(default_options): - """Verify we update the line number and reset multiline.""" - file_processor = processor.FileProcessor( - "-", default_options, lines=["a = 1\n"] - ) - - assert file_processor.multiline is False - assert file_processor.line_number == 0 - with file_processor.inside_multiline(10): - assert file_processor.multiline is True - assert file_processor.line_number == 10 - - assert file_processor.multiline is False - - @pytest.mark.parametrize( "string, expected", [ From cc301ed499511c56ceca3262a2302364dc146b06 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 14:36:44 -0400 Subject: [PATCH 198/257] fix pylintrc warnings --- .pylintrc | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.pylintrc b/.pylintrc index a23de97..92ea2a4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -365,10 +365,3 @@ ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception From 5233d880696a6867b53fabf0c9d4419d83f372ad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 14:39:06 -0400 Subject: [PATCH 199/257] use setup-cfg-fmt --- .pre-commit-config.yaml | 4 ++++ setup.cfg | 11 +++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dbe6918..6de9c1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,10 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace exclude: ^tests/fixtures/ +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v2.4.0 + hooks: + - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports rev: v3.10.0 hooks: diff --git a/setup.cfg b/setup.cfg index 593c2e1..ebf4355 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ author_email = tarek@ziade.org maintainer = Ian Stapleton Cordasco maintainer_email = graffatcolmingov@gmail.com license = MIT -license_file = LICENSE +license_files = LICENSE classifiers = Development Status :: 5 - Production/Stable Environment :: Console @@ -27,18 +27,13 @@ classifiers = [options] packages = find: -package_dir = - =src -# We document the reasoning for using ranges here: -# https://flake8.pycqa.org/en/latest/faq.html#why-does-flake8-use-ranges-for-its-dependencies -# And in which releases we will update those ranges here: -# https://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.11.0,<2.12.0 pyflakes>=3.1.0,<3.2.0 -# 3.8.0's importlib.metadata is broken python_requires = >=3.8.1 +package_dir = + =src [options.packages.find] where = src From 7ef0350a439c93166bc8ba89fcc3de6a9a664e6c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 15:04:17 -0400 Subject: [PATCH 200/257] Release 6.1.0 --- docs/source/internal/releases.rst | 4 ++-- docs/source/release-notes/6.1.0.rst | 22 ++++++++++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 docs/source/release-notes/6.1.0.rst diff --git a/docs/source/internal/releases.rst b/docs/source/internal/releases.rst index 39fbdf7..fc29bd3 100644 --- a/docs/source/internal/releases.rst +++ b/docs/source/internal/releases.rst @@ -81,9 +81,9 @@ for users. Before releasing, the following tox test environments must pass: -- Python 3.6 (a.k.a., ``tox -e py36``) +- Python 3.8 (a.k.a., ``tox -e py38``) -- Python 3.7 (a.k.a., ``tox -e py37``) +- Python 3.12 (a.k.a., ``tox -e py312``) - PyPy 3 (a.k.a., ``tox -e pypy3``) diff --git a/docs/source/release-notes/6.1.0.rst b/docs/source/release-notes/6.1.0.rst new file mode 100644 index 0000000..a2244a9 --- /dev/null +++ b/docs/source/release-notes/6.1.0.rst @@ -0,0 +1,22 @@ +6.1.0 -- 2023-07-29 +------------------- + +You can view the `6.1.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Pyflakes has been updated to >= 3.1.0, < 3.2.0 (See also :pull:`1847`). +- pycodestyle has been updated to >= 2.11.0, < 2.12.0 (See also :pull:`1848`). + +Features +~~~~~~~~ + +- Deprecate ``--include-in-doctest``, ``--exclude-from-doctest`` (See also + :issue:`1747`, :pull:`1768`). +- Add support for python 3.12 (See also :pull:`1832`, :pull:`1849`, + :pull:`1850`). + +.. all links +.. _6.1.0 milestone: + https://github.com/PyCQA/flake8/milestone/48 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index a9eff5f..8cd7573 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -10,6 +10,7 @@ with the newest releases first. .. toctree:: 6.0.0 + 6.1.0 5.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index c6d48b7..171b1db 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "6.0.0" +__version__ = "6.1.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 4a47bab979b251324aa31fc3db61e64c8ed3d15d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 15:43:21 -0400 Subject: [PATCH 201/257] improve coverage --- .coveragerc | 26 ------------- setup.cfg | 9 +++++ src/flake8/_compat.py | 4 +- src/flake8/processor.py | 81 +++++++++++++++++------------------------ tox.ini | 1 + 5 files changed, 45 insertions(+), 76 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index d74ca19..0000000 --- a/.coveragerc +++ /dev/null @@ -1,26 +0,0 @@ -[run] -branch = True -source = - flake8 - tests -omit = - # Don't complain if non-runnable code isn't run - */__main__.py - -[report] -show_missing = True -skip_covered = True -exclude_lines = - # Have to re-enable the standard pragma - \#\s*pragma: no cover - - # Don't complain if tests don't hit defensive assertion code: - ^\s*raise AssertionError\b - ^\s*raise NotImplementedError\b - ^\s*return NotImplemented\b - ^\s*raise$ - - # Don't complain if non-runnable code isn't run: - ^if __name__ == ['"]__main__['"]:$ - ^\s*if False: - ^\s*if TYPE_CHECKING: diff --git a/setup.cfg b/setup.cfg index ebf4355..70c2b8d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,15 @@ flake8.report = [bdist_wheel] universal = 1 +[coverage:run] +source = + flake8 + tests +plugins = covdefaults + +[coverage:report] +fail_under = 97 + [mypy] check_untyped_defs = true disallow_any_generics = true diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py index f4a0903..e8a3ccd 100644 --- a/src/flake8/_compat.py +++ b/src/flake8/_compat.py @@ -3,9 +3,9 @@ from __future__ import annotations import sys import tokenize -if sys.version_info >= (3, 12): +if sys.version_info >= (3, 12): # pragma: >=3.12 cover FSTRING_START = tokenize.FSTRING_START FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE FSTRING_END = tokenize.FSTRING_END -else: +else: # pragma: <3.12 cover FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1 diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 2eea88f..21a25e0 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import ast +import functools import logging import tokenize from typing import Any @@ -114,23 +115,15 @@ class FileProcessor: self.verbose = options.verbose #: Statistics dictionary self.statistics = {"logical lines": 0} - self._file_tokens: list[tokenize.TokenInfo] | None = None - # map from line number to the line we'll search for `noqa` in - self._noqa_line_mapping: dict[int, str] | None = None self._fstring_start = -1 - @property + @functools.cached_property def file_tokens(self) -> list[tokenize.TokenInfo]: """Return the complete set of tokens for a file.""" - if self._file_tokens is None: - line_iter = iter(self.lines) - self._file_tokens = list( - tokenize.generate_tokens(lambda: next(line_iter)) - ) + line_iter = iter(self.lines) + return list(tokenize.generate_tokens(lambda: next(line_iter))) - return self._file_tokens - - def fstring_start(self, lineno: int) -> None: + def fstring_start(self, lineno: int) -> None: # pragma: >=3.12 cover """Signal the beginning of an fstring.""" self._fstring_start = lineno @@ -138,7 +131,7 @@ class FileProcessor: self, token: tokenize.TokenInfo ) -> Generator[str, None, None]: """Iterate through the lines of a multiline string.""" - if token.type == FSTRING_END: + if token.type == FSTRING_END: # pragma: >=3.12 cover start = self._fstring_start else: start = token.start[0] @@ -209,7 +202,7 @@ class FileProcessor: continue if token_type == tokenize.STRING: text = mutate_string(text) - elif token_type == FSTRING_MIDDLE: + elif token_type == FSTRING_MIDDLE: # pragma: >=3.12 cover text = "x" * len(text) if previous_row: (start_row, start_column) = start @@ -277,41 +270,37 @@ class FileProcessor: joined = "".join(self.lines[min_line - 1 : max_line]) return dict.fromkeys(line_range, joined) - def noqa_line_for(self, line_number: int) -> str | None: - """Retrieve the line which will be used to determine noqa.""" - if self._noqa_line_mapping is None: - try: - file_tokens = self.file_tokens - except (tokenize.TokenError, SyntaxError): - # if we failed to parse the file tokens, we'll always fail in - # the future, so set this so the code does not try again - self._noqa_line_mapping = {} - else: - ret = {} + @functools.cached_property + def _noqa_line_mapping(self) -> dict[int, str]: + """Map from line number to the line we'll search for `noqa` in.""" + try: + file_tokens = self.file_tokens + except (tokenize.TokenError, SyntaxError): + # if we failed to parse the file tokens, we'll always fail in + # the future, so set this so the code does not try again + return {} + else: + ret = {} - min_line = len(self.lines) + 2 - max_line = -1 - for tp, _, (s_line, _), (e_line, _), _ in file_tokens: - if tp == tokenize.ENDMARKER: - break + min_line = len(self.lines) + 2 + max_line = -1 + for tp, _, (s_line, _), (e_line, _), _ in file_tokens: + if tp == tokenize.ENDMARKER or tp == tokenize.DEDENT: + continue - min_line = min(min_line, s_line) - max_line = max(max_line, e_line) + min_line = min(min_line, s_line) + max_line = max(max_line, e_line) - if tp in (tokenize.NL, tokenize.NEWLINE): - ret.update(self._noqa_line_range(min_line, max_line)) - - min_line = len(self.lines) + 2 - max_line = -1 - - # in newer versions of python, a `NEWLINE` token is inserted - # at the end of the file even if it doesn't have one. - # on old pythons, they will not have hit a `NEWLINE` - if max_line != -1: + if tp in (tokenize.NL, tokenize.NEWLINE): ret.update(self._noqa_line_range(min_line, max_line)) - self._noqa_line_mapping = ret + min_line = len(self.lines) + 2 + max_line = -1 + return ret + + def noqa_line_for(self, line_number: int) -> str | None: + """Retrieve the line which will be used to determine noqa.""" # NOTE(sigmavirus24): Some plugins choose to report errors for empty # files on Line 1. In those cases, we shouldn't bother trying to # retrieve a physical line (since none exist). @@ -377,12 +366,8 @@ class FileProcessor: # If we have nothing to analyze quit early return - first_byte = ord(self.lines[0][0]) - if first_byte not in (0xEF, 0xFEFF): - return - # If the first byte of the file is a UTF-8 BOM, strip it - if first_byte == 0xFEFF: + if self.lines[0][:1] == "\uFEFF": self.lines[0] = self.lines[0][1:] elif self.lines[0][:3] == "\xEF\xBB\xBF": self.lines[0] = self.lines[0][3:] diff --git a/tox.ini b/tox.ini index aae002d..539b5c4 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = py,flake8,linters,docs deps = pytest!=3.0.5,!=5.2.3 coverage>=6 + covdefaults commands = coverage run -m pytest {posargs} coverage report From b3e251512238ad39c87e4b8d1614903fe493f7ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2023 15:53:22 -0400 Subject: [PATCH 202/257] remove --include-in-doctest and --exclude-in-doctest --- docs/source/user/options.rst | 60 ------------------------ src/flake8/plugins/pyflakes.py | 85 ++-------------------------------- 2 files changed, 3 insertions(+), 142 deletions(-) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index 8db2df8..d767748 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -98,10 +98,6 @@ Index of Options - :option:`flake8 --doctests` -- :option:`flake8 --include-in-doctest` - -- :option:`flake8 --exclude-from-doctest` - - :option:`flake8 --benchmark` - :option:`flake8 --bug-report` @@ -997,62 +993,6 @@ Options and their Descriptions doctests = True -.. option:: --include-in-doctest= - - :ref:`Go back to index ` - - Specify which files are checked by PyFlakes for doctest syntax. - - This is registered by the default PyFlakes plugin. - - Command-line example: - - .. prompt:: bash - - flake8 --include-in-doctest=dir/subdir/file.py,dir/other/file.py dir/ - - This **can** be specified in config files. - - Example config file usage: - - .. code-block:: ini - - include-in-doctest = - dir/subdir/file.py, - dir/other/file.py - include_in_doctest = - dir/subdir/file.py, - dir/other/file.py - - -.. option:: --exclude-from-doctest= - - :ref:`Go back to index ` - - Specify which files are not to be checked by PyFlakes for doctest syntax. - - This is registered by the default PyFlakes plugin. - - Command-line example: - - .. prompt:: bash - - flake8 --exclude-from-doctest=dir/subdir/file.py,dir/other/file.py dir/ - - This **can** be specified in config files. - - Example config file usage: - - .. code-block:: ini - - exclude-from-doctest = - dir/subdir/file.py, - dir/other/file.py - exclude_from_doctest = - dir/subdir/file.py, - dir/other/file.py - - .. option:: --benchmark :ref:`Go back to index ` diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index f62527e..6c57619 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -4,13 +4,11 @@ from __future__ import annotations import argparse import ast import logging -import os from typing import Any from typing import Generator import pyflakes.checker -from flake8 import utils from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) @@ -68,34 +66,12 @@ class FlakesChecker(pyflakes.checker.Checker): """Subclass the Pyflakes checker to conform with the flake8 API.""" with_doctest = False - include_in_doctest: list[str] = [] - exclude_from_doctest: list[str] = [] def __init__(self, tree: ast.AST, filename: str) -> None: """Initialize the PyFlakes plugin with an AST tree and filename.""" - filename = utils.normalize_path(filename) - with_doctest = self.with_doctest - included_by = [ - include - for include in self.include_in_doctest - if include != "" and filename.startswith(include) - ] - if included_by: - with_doctest = True - - for exclude in self.exclude_from_doctest: - if exclude != "" and filename.startswith(exclude): - with_doctest = False - overlapped_by = [ - include - for include in included_by - if include.startswith(exclude) - ] - - if overlapped_by: - with_doctest = True - - super().__init__(tree, filename=filename, withDoctest=with_doctest) + super().__init__( + tree, filename=filename, withDoctest=self.with_doctest + ) @classmethod def add_options(cls, parser: OptionManager) -> None: @@ -113,24 +89,6 @@ class FlakesChecker(pyflakes.checker.Checker): parse_from_config=True, help="also check syntax of the doctests", ) - parser.add_option( - "--include-in-doctest", - default="", - dest="include_in_doctest", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - help="Run doctests only on these files", - ) - parser.add_option( - "--exclude-from-doctest", - default="", - dest="exclude_from_doctest", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - help="Skip these files when running doctests", - ) @classmethod def parse_options(cls, options: argparse.Namespace) -> None: @@ -139,43 +97,6 @@ class FlakesChecker(pyflakes.checker.Checker): cls.builtIns = cls.builtIns.union(options.builtins) cls.with_doctest = options.doctests - if options.include_in_doctest or options.exclude_from_doctest: - LOG.warning( - "--include-in-doctest / --exclude-from-doctest will be " - "removed in a future version. see PyCQA/flake8#1747" - ) - - included_files = [] - for included_file in options.include_in_doctest: - if included_file == "": - continue - if not included_file.startswith((os.sep, "./", "~/")): - included_files.append(f"./{included_file}") - else: - included_files.append(included_file) - cls.include_in_doctest = utils.normalize_paths(included_files) - - excluded_files = [] - for excluded_file in options.exclude_from_doctest: - if excluded_file == "": - continue - if not excluded_file.startswith((os.sep, "./", "~/")): - excluded_files.append(f"./{excluded_file}") - else: - excluded_files.append(excluded_file) - cls.exclude_from_doctest = utils.normalize_paths(excluded_files) - - inc_exc = set(cls.include_in_doctest).intersection( - cls.exclude_from_doctest - ) - if inc_exc: - raise ValueError( - f"{inc_exc!r} was specified in both the " - f"include-in-doctest and exclude-from-doctest " - f"options. You are not allowed to specify it in " - f"both for doctesting." - ) - def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]: """Run the plugin.""" for message in self.messages: From d734e316890aad2cab1ad02e8ba8a277f23d7dfb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2023 19:27:59 -0400 Subject: [PATCH 203/257] add minimal rtd configuration --- .readthedocs.yaml | 10 ++++++++++ docs/source/requirements.txt | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..0425dc2 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" +python: + install: + - path: . + - requirements: docs/source/requirements.txt diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index da3b991..93f773e 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1,4 +1,4 @@ sphinx>=2.1.0,!=3.1.0 -sphinx_rtd_theme -sphinx-prompt +sphinx-rtd-theme>=1.2.2 +sphinx-prompt>=1.5.0 docutils!=0.18 From 5cd0bcb45a22e490545d8741e57557bf4400f488 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:27:47 +0000 Subject: [PATCH 204/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.9.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.9.0...v3.10.1) - [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6de9c1b..0c8544e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.9.0 + rev: v3.10.1 hooks: - id: pyupgrade args: [--py38-plus] @@ -31,7 +31,7 @@ repos: - id: black args: [--line-length=79] - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 5a5ebaf10a5996de1d759f7088ca9aaec2175f12 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 10:33:21 +0000 Subject: [PATCH 205/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.4.1 → v1.5.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.4.1...v1.5.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c8544e..10c91d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From aceddfeabb6a59b8cef318ac29bab15a8bc78b6f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 11:03:53 +0000 Subject: [PATCH 206/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10c91d1..1bcfab6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.0 + rev: v1.5.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From a0f393ca71c7e19b7cf2a30e801f84ceb3c12106 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:10:16 +0000 Subject: [PATCH 207/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bcfab6..1394545 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black args: [--line-length=79] From 7899a82c5de1489849e722905661f703d89ece7f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:01:32 +0000 Subject: [PATCH 208/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.10.0 → v3.11.0](https://github.com/asottile/reorder-python-imports/compare/v3.10.0...v3.11.0) - [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1394545..5969610 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.10.0 + rev: v3.11.0 hooks: - id: reorder-python-imports args: [ @@ -21,7 +21,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py38-plus] From fb5759b37facaee1b8db4a7666ee6b82d2293ee0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:18:31 +0000 Subject: [PATCH 209/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.11.0 → v3.13.0](https://github.com/asottile/pyupgrade/compare/v3.11.0...v3.13.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5969610..4783496 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.11.0 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] From 0a67cbc61ff6fece30b3be1d6e089bbc04cc3e5f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:57:35 +0000 Subject: [PATCH 210/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.4.0 → v2.5.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.4.0...v2.5.0) - [github.com/asottile/reorder-python-imports: v3.11.0 → v3.12.0](https://github.com/asottile/reorder-python-imports/compare/v3.11.0...v3.12.0) - [github.com/asottile/pyupgrade: v3.13.0 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4783496..b51f834 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.4.0 + rev: v2.5.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.11.0 + rev: v3.12.0 hooks: - id: reorder-python-imports args: [ @@ -21,7 +21,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] From 67c0ecc6df951cf3e37f41db935cb7390c1c1678 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 21:55:48 +0000 Subject: [PATCH 211/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) - [github.com/asottile/pyupgrade: v3.14.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b51f834..595cae4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml - id: debug-statements @@ -21,7 +21,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] From 8bdec0b54ef230e9d0fa5ca9554d4a3d6a764cf5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:14:00 +0000 Subject: [PATCH 212/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 595cae4..372a9ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 61d6ca224df709be82fd370bf1a84d0ddd69965f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:45:49 +0000 Subject: [PATCH 213/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1) - [github.com/pre-commit/mirrors-mypy: v1.6.0 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.0...v1.6.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 372a9ec..92467ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black args: [--line-length=79] @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.0 + rev: v1.6.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From d1b1ec73be7ecbf3cbc55a1a5d07d8e37c725ee1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:55:32 +0000 Subject: [PATCH 214/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92467ac..3dbc531 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black args: [--line-length=79] @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 7e1c87554df99a3c779747b30e6a52aace323d4f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:09:51 +0000 Subject: [PATCH 215/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3dbc531..fa2ea66 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 + rev: v1.7.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From c8801c129ab3138c4f3db4841d76bb30ed8e3f8c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 21:54:28 +0000 Subject: [PATCH 216/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.11.0 → 23.12.0](https://github.com/psf/black/compare/23.11.0...23.12.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa2ea66..44f785d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black args: [--line-length=79] From b67ce03a4a9c9902fea163021a844f34287ee6bc Mon Sep 17 00:00:00 2001 From: Ian Stapleton Cordasco Date: Wed, 20 Dec 2023 06:54:05 -0600 Subject: [PATCH 217/257] Fix bugbear lints --- src/flake8/checker.py | 3 ++- tests/unit/plugins/reporter_test.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 6c4caef..39c43e3 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -70,7 +70,8 @@ def _mp_init(argv: Sequence[str]) -> None: signal.signal(signal.SIGINT, signal.SIG_IGN) try: - _mp_plugins, _mp_options # for `fork` this'll already be set + # for `fork` this'll already be set + _mp_plugins, _mp_options # noqa: B018 except NameError: plugins, options = parse_args(argv) _mp_plugins, _mp_options = plugins.checkers, options diff --git a/tests/unit/plugins/reporter_test.py b/tests/unit/plugins/reporter_test.py index ff4d97f..842465a 100644 --- a/tests/unit/plugins/reporter_test.py +++ b/tests/unit/plugins/reporter_test.py @@ -11,7 +11,7 @@ from flake8.plugins import reporter def _opts(**kwargs): - kwargs.setdefault("quiet", 0), + kwargs.setdefault("quiet", 0) kwargs.setdefault("color", "never") kwargs.setdefault("output_file", None) return argparse.Namespace(**kwargs) From 06c1503842ee90a4cca5ed57908c0f27595a6f4d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 12:54:27 +0000 Subject: [PATCH 218/257] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/flake8/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 39c43e3..329a2cc 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -71,7 +71,7 @@ def _mp_init(argv: Sequence[str]) -> None: try: # for `fork` this'll already be set - _mp_plugins, _mp_options # noqa: B018 + _mp_plugins, _mp_options # noqa: B018 except NameError: plugins, options = parse_args(argv) _mp_plugins, _mp_options = plugins.checkers, options From e27611f1eadc16a5bd02125aa8a054c632d3b0c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 21:35:51 +0000 Subject: [PATCH 219/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.12.0 → 23.12.1](https://github.com/psf/black/compare/23.12.0...23.12.1) - [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44f785d..bbb1833 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black args: [--line-length=79] @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From cde8570df3bf4b647dfa65a97613fb325a9f1bbd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 4 Jan 2024 19:36:48 -0500 Subject: [PATCH 220/257] upgrade pyflakes to 3.2.x --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 70c2b8d..2254902 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ packages = find: install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.11.0,<2.12.0 - pyflakes>=3.1.0,<3.2.0 + pyflakes>=3.2.0,<3.3.0 python_requires = >=3.8.1 package_dir = =src From 88a4f9b2f48fc44b025a48fa6a8ac7cc89ef70e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 4 Jan 2024 19:41:07 -0500 Subject: [PATCH 221/257] Release 7.0.0 --- docs/source/internal/releases.rst | 2 +- docs/source/release-notes/7.0.0.rst | 19 +++++++++++++++++++ docs/source/release-notes/index.rst | 6 ++++++ src/flake8/__init__.py | 2 +- 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 docs/source/release-notes/7.0.0.rst diff --git a/docs/source/internal/releases.rst b/docs/source/internal/releases.rst index fc29bd3..0081509 100644 --- a/docs/source/internal/releases.rst +++ b/docs/source/internal/releases.rst @@ -30,7 +30,7 @@ Historically, |Flake8| has generated major releases for: - Large scale refactoring (2.0, 3.0, 5.0, 6.0) -- Subtly breaking CLI changes (3.0, 4.0, 5.0, 6.0) +- Subtly breaking CLI changes (3.0, 4.0, 5.0, 6.0, 7.0) - Breaking changes to its plugin interface (3.0) diff --git a/docs/source/release-notes/7.0.0.rst b/docs/source/release-notes/7.0.0.rst new file mode 100644 index 0000000..6cd852a --- /dev/null +++ b/docs/source/release-notes/7.0.0.rst @@ -0,0 +1,19 @@ +7.0.0 -- 2024-01-04 +------------------- + +You can view the `7.0.0 milestone`_ on GitHub for more details. + +Backwards Incompatible Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove ``--include-in-doctest`` and ``--exclude-from-doctest`` options. + (See also :issue:`1747`, :pull:`1854`) + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Pyflakes has been updated to >= 3.2.0, < 3.3.0 (See also :pull:`1906`). + +.. all links +.. _7.0.0 milestone: + https://github.com/PyCQA/flake8/milestone/49 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 8cd7573..58c6845 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -5,6 +5,12 @@ All of the release notes that have been recorded for Flake8 are organized here with the newest releases first. +7.x Release Series +================== + +.. toctree:: + 7.0.0 + 6.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 171b1db..ea7a56d 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "6.1.0" +__version__ = "7.0.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 26d3184ae2432eb382980590c77b47bb393ce4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 5 Jan 2024 12:14:44 +0100 Subject: [PATCH 222/257] update plugins for sphinx-prompt-1.8.0 The sphinx-prompt plugin has renamed its package in 1.8.0 from erraneous `sphinx-prompt` name to `sphinx_prompt`. Adjust the conf accordingly. --- docs/source/conf.py | 2 +- docs/source/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index a2b4af3..9b7aa5a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -34,7 +34,7 @@ extensions = [ "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.viewcode", - "sphinx-prompt", + "sphinx_prompt", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index 93f773e..765fb13 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1,4 +1,4 @@ sphinx>=2.1.0,!=3.1.0 sphinx-rtd-theme>=1.2.2 -sphinx-prompt>=1.5.0 +sphinx-prompt>=1.8.0 docutils!=0.18 From a2b68c84e760a4906ad47eac84d277c85aaf8322 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:35:25 +0000 Subject: [PATCH 223/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbb1833..ad60be1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - id: black args: [--line-length=79] - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 49f52a8598d8a934b07f367a1b3ad87dbe51be5b Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:06:23 +0100 Subject: [PATCH 224/257] Update documentation regarding entry points Refer to the PyPA packaging guide Replace references to the deprecated `pkg_resources` docs --- docs/source/conf.py | 6 +++++- docs/source/plugin-development/index.rst | 7 ++----- .../registering-plugins.rst | 21 ++++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9b7aa5a..48f8a52 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -296,7 +296,11 @@ texinfo_documents = [ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "packaging": ("https://packaging.python.org/en/latest/", None), + "setuptools": ("https://setuptools.pypa.io/en/latest/", None), +} extlinks = { "issue": ("https://github.com/pycqa/flake8/issues/%s", "#%s"), diff --git a/docs/source/plugin-development/index.rst b/docs/source/plugin-development/index.rst index c89e5f0..82f4a6b 100644 --- a/docs/source/plugin-development/index.rst +++ b/docs/source/plugin-development/index.rst @@ -30,7 +30,8 @@ To get started writing a |Flake8| :term:`plugin` you first need: Once you've gathered these things, you can get started. -All plugins for |Flake8| must be registered via `entry points`_. In this +All plugins for |Flake8| must be registered via +:external:doc:`entry points`. In this section we cover: - How to register your plugin so |Flake8| can find it @@ -62,7 +63,3 @@ Here's a tutorial which goes over building an ast checking plugin from scratch: registering-plugins plugin-parameters formatters - - -.. _entry points: - https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index ca74008..e5568b8 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -12,16 +12,15 @@ To register any kind of plugin with |Flake8|, you need: #. A name for your plugin that will (ideally) be unique. -#. A somewhat recent version of setuptools (newer than 0.7.0 but preferably as - recent as you can attain). - -|Flake8| relies on functionality provided by setuptools called -`Entry Points`_. These allow any package to register a plugin with |Flake8| -via that package's ``setup.py`` file. +|Flake8| relies on functionality provided by build tools called +:external:doc:`entry points`. These allow +any package to register a plugin with |Flake8| via that package's metadata. Let's presume that we already have our plugin written and it's in a module -called ``flake8_example``. We might have a ``setup.py`` that looks something -like: +called ``flake8_example``. We will also assume ``setuptools`` is used as a +:term:`Build Backend`, but be aware that most backends support entry points. + +We might have a ``setup.py`` that looks something like: .. code-block:: python @@ -150,5 +149,7 @@ If your plugin is intended to be opt-in, it can set the attribute :ref:`enable-extensions` with your plugin's entry point. -.. _Entry Points: - https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points +.. seealso:: + + The :external:doc:`setuptools user guide ` about + entry points. From 350f2545fd3ec75640a1605e4995a2f921e8b38b Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Sun, 17 Mar 2024 08:56:07 +0100 Subject: [PATCH 225/257] Use explicit external references --- docs/source/plugin-development/index.rst | 2 +- .../plugin-development/registering-plugins.rst | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/source/plugin-development/index.rst b/docs/source/plugin-development/index.rst index 82f4a6b..9b38fe2 100644 --- a/docs/source/plugin-development/index.rst +++ b/docs/source/plugin-development/index.rst @@ -31,7 +31,7 @@ To get started writing a |Flake8| :term:`plugin` you first need: Once you've gathered these things, you can get started. All plugins for |Flake8| must be registered via -:external:doc:`entry points`. In this +:external+packaging:doc:`entry points`. In this section we cover: - How to register your plugin so |Flake8| can find it diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index e5568b8..41c34e0 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -13,12 +13,14 @@ To register any kind of plugin with |Flake8|, you need: #. A name for your plugin that will (ideally) be unique. |Flake8| relies on functionality provided by build tools called -:external:doc:`entry points`. These allow -any package to register a plugin with |Flake8| via that package's metadata. +:external+packaging:doc:`entry points`. These +allow any package to register a plugin with |Flake8| via that package's +metadata. Let's presume that we already have our plugin written and it's in a module +:external+packaging:term:`Build Backend`, but be aware that most backends called ``flake8_example``. We will also assume ``setuptools`` is used as a -:term:`Build Backend`, but be aware that most backends support entry points. +support entry points. We might have a ``setup.py`` that looks something like: @@ -151,5 +153,5 @@ point. .. seealso:: - The :external:doc:`setuptools user guide ` about - entry points. + The :external+setuptools:doc:`setuptools user guide ` + about entry points. From ba0f56610adbd4d8733772ce1c63efcab1b70079 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Sun, 17 Mar 2024 08:56:07 +0100 Subject: [PATCH 226/257] Use explicit external references --- docs/source/plugin-development/registering-plugins.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index 41c34e0..aac87e8 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -20,6 +20,7 @@ metadata. Let's presume that we already have our plugin written and it's in a module :external+packaging:term:`Build Backend`, but be aware that most backends called ``flake8_example``. We will also assume ``setuptools`` is used as a +:external+packaging:term:`Build Backend`, but be aware that most backends support entry points. We might have a ``setup.py`` that looks something like: From 6659b213c9aa8fa49235e13a365fcd34f58cbc6b Mon Sep 17 00:00:00 2001 From: Ian Stapleton Cordasco Date: Mon, 18 Mar 2024 10:43:13 -0500 Subject: [PATCH 227/257] Fix toctree ordering in index Also fix line-length issue in PR --- docs/source/plugin-development/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/plugin-development/index.rst b/docs/source/plugin-development/index.rst index 9b38fe2..9088942 100644 --- a/docs/source/plugin-development/index.rst +++ b/docs/source/plugin-development/index.rst @@ -55,6 +55,8 @@ Here's a tutorial which goes over building an ast checking plugin from scratch: +Detailed Plugin Development Documentation +========================================= .. toctree:: :caption: Plugin Developer Documentation From 866ad729c64eea359960a8ac4e3f1201104ee55c Mon Sep 17 00:00:00 2001 From: mzagol Date: Mon, 15 Apr 2024 15:52:25 -0500 Subject: [PATCH 228/257] Add --extend-exclude to the TOC --- docs/source/user/options.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index d767748..bd80c87 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -46,6 +46,8 @@ Index of Options - :option:`flake8 --exclude` +- :option:`flake8 --extend-exclude` + - :option:`flake8 --filename` - :option:`flake8 --stdin-display-name` From defd315175b7b77472affb61a410e5720dabdc1a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 15 Jun 2024 17:30:39 -0400 Subject: [PATCH 229/257] latest pycodestyle --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2254902..28e6f93 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ classifiers = packages = find: install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.11.0,<2.12.0 + pycodestyle>=2.12.0,<2.13.0 pyflakes>=3.2.0,<3.3.0 python_requires = >=3.8.1 package_dir = From 65a38c42a7f1a05ff8d99b313160754fc9b7a0d8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 15 Jun 2024 17:36:14 -0400 Subject: [PATCH 230/257] Release 7.1.0 --- docs/source/release-notes/7.1.0.rst | 13 +++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/7.1.0.rst diff --git a/docs/source/release-notes/7.1.0.rst b/docs/source/release-notes/7.1.0.rst new file mode 100644 index 0000000..2229baa --- /dev/null +++ b/docs/source/release-notes/7.1.0.rst @@ -0,0 +1,13 @@ +7.1.0 -- 2024-06-15 +------------------- + +You can view the `7.1.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.12.0, < 2.13.0 (See also :pull:`1939`). + +.. all links +.. _7.1.0 milestone: + https://github.com/PyCQA/flake8/milestone/50 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 58c6845..4ae650a 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -10,6 +10,7 @@ with the newest releases first. .. toctree:: 7.0.0 + 7.1.0 6.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index ea7a56d..19df059 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.0.0" +__version__ = "7.1.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 10314ad9e5236f1ddf70cb25c2854c93c0840b66 Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:39:49 +0200 Subject: [PATCH 231/257] Fix wording of plugins documentation --- docs/source/plugin-development/registering-plugins.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index aac87e8..964ff99 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -18,7 +18,6 @@ allow any package to register a plugin with |Flake8| via that package's metadata. Let's presume that we already have our plugin written and it's in a module -:external+packaging:term:`Build Backend`, but be aware that most backends called ``flake8_example``. We will also assume ``setuptools`` is used as a :external+packaging:term:`Build Backend`, but be aware that most backends support entry points. From bdcd5c2c0afadaf7c92a4b26d96055cecdd38cf3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Aug 2024 11:08:06 +0100 Subject: [PATCH 232/257] Handle escaped braces in f-strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To use a curly brace in an f-string, you must escape it. For example: >>> k = 1 >>> f'{{{k}' '{1' Saving this as a script and running the 'tokenize' module highlights something odd around the counting of tokens: ❯ python -m tokenize wow.py 0,0-0,0: ENCODING 'utf-8' 1,0-1,1: NAME 'k' 1,2-1,3: OP '=' 1,4-1,5: NUMBER '1' 1,5-1,6: NEWLINE '\n' 2,0-2,2: FSTRING_START "f'" 2,2-2,3: FSTRING_MIDDLE '{' # <-- here... 2,4-2,5: OP '{' # <-- and here 2,5-2,6: NAME 'k' 2,6-2,7: OP '}' 2,7-2,8: FSTRING_END "'" 2,8-2,9: NEWLINE '\n' 3,0-3,0: ENDMARKER '' The FSTRING_MIDDLE character we have is the escaped/post-parse single curly brace rather than the raw double curly brace, however, while our end index of this token accounts for the parsed form, the start index of the next token does not (put another way, it jumps from 3 -> 4). This triggers some existing, unrelated code that we need to bypass. Do just that. Signed-off-by: Stephen Finucane Closes: #1948 --- src/flake8/processor.py | 8 ++++++- tests/integration/test_plugins.py | 35 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 21a25e0..e44547b 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -203,7 +203,13 @@ class FileProcessor: if token_type == tokenize.STRING: text = mutate_string(text) elif token_type == FSTRING_MIDDLE: # pragma: >=3.12 cover - text = "x" * len(text) + # A curly brace in an FSTRING_MIDDLE token must be an escaped + # curly brace. Both 'text' and 'end' will account for the + # escaped version of the token (i.e. a single brace) rather + # than the raw double brace version, so we must counteract this + brace_offset = text.count("{") + text.count("}") + text = "x" * (len(text) + brace_offset) + end = (end[0], end[1] + brace_offset) if previous_row: (start_row, start_column) = start if previous_row != start_row: diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index d4c22b0..90ca555 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -1,6 +1,8 @@ """Integration tests for plugin loading.""" from __future__ import annotations +import sys + import pytest from flake8.main.cli import main @@ -261,3 +263,36 @@ t.py:1:1: T001 "f'xxxxxxxxxxx'" """ out, err = capsys.readouterr() assert out == expected + + +def test_escaping_of_fstrings_in_string_redacter(tmpdir, capsys): + cfg_s = f"""\ +[flake8] +extend-ignore = F +[flake8:local-plugins] +extension = + T = {yields_logical_line.__module__}:{yields_logical_line.__name__} +""" + + cfg = tmpdir.join("tox.ini") + cfg.write(cfg_s) + + src = """\ +f'{{"{hello}": "{world}"}}' +""" + t_py = tmpdir.join("t.py") + t_py.write_binary(src.encode()) + + with tmpdir.as_cwd(): + assert main(("t.py", "--config", str(cfg))) == 1 + + if sys.version_info >= (3, 12): # pragma: >=3.12 cover + expected = """\ +t.py:1:1: T001 "f'xxx{hello}xxxx{world}xxx'" +""" + else: # pragma: <3.12 cover + expected = """\ +t.py:1:1: T001 "f'xxxxxxxxxxxxxxxxxxxxxxxx'" +""" + out, err = capsys.readouterr() + assert out == expected From cf1542cefa3e766670b2066dd75c4571d682a649 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Aug 2024 16:31:56 -0400 Subject: [PATCH 233/257] Release 7.1.1 --- docs/source/release-notes/7.1.1.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/7.1.1.rst diff --git a/docs/source/release-notes/7.1.1.rst b/docs/source/release-notes/7.1.1.rst new file mode 100644 index 0000000..62f2d11 --- /dev/null +++ b/docs/source/release-notes/7.1.1.rst @@ -0,0 +1,15 @@ +7.1.1 -- 2024-08-04 +------------------- + +You can view the `7.1.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Properly preserve escaped `{` and `}` in fstrings in logical lines in 3.12+. + (See also :issue:`1948`, :pull:`1949`). + + +.. all links +.. _7.1.1 milestone: + https://github.com/PyCQA/flake8/milestone/51 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 4ae650a..9bf8646 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -11,6 +11,7 @@ with the newest releases first. .. toctree:: 7.0.0 7.1.0 + 7.1.1 6.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 19df059..101eafe 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.1.0" +__version__ = "7.1.1" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From f35737a32d8283fd0d3456bd9919718ff2d1b077 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Feb 2025 13:29:05 -0500 Subject: [PATCH 234/257] avoid starting unnecessary processes when file count is limited --- src/flake8/checker.py | 1 + tests/unit/test_checker_manager.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 329a2cc..ff62679 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -256,6 +256,7 @@ class Manager: exclude=self.exclude, ) ) + self.jobs = min(len(self.filenames), self.jobs) def stop(self) -> None: """Stop checking files.""" diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 68dd82a..8d6b3dc 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -61,6 +61,16 @@ def test_multiprocessing_cpu_count_not_implemented(): assert manager.jobs == 0 +def test_jobs_count_limited_to_file_count(): + style_guide = style_guide_mock() + style_guide.options.jobs = JobsArgument("4") + style_guide.options.filenames = ["file1", "file2"] + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + assert manager.jobs == 4 + manager.start() + assert manager.jobs == 2 + + def test_make_checkers(): """Verify that we create a list of FileChecker instances.""" style_guide = style_guide_mock() From fffee8ba9dc5903484f99390e6c7f4bbef59bda7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Feb 2025 13:48:15 -0500 Subject: [PATCH 235/257] Release 7.1.2 --- docs/source/release-notes/7.1.2.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/7.1.2.rst diff --git a/docs/source/release-notes/7.1.2.rst b/docs/source/release-notes/7.1.2.rst new file mode 100644 index 0000000..010656c --- /dev/null +++ b/docs/source/release-notes/7.1.2.rst @@ -0,0 +1,15 @@ +7.1.2 -- 2025-02-16 +------------------- + +You can view the `7.1.2 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Avoid starting unnecessary processes when "# files" < "jobs". + (See also :pull:`1966`). + + +.. all links +.. _7.1.2 milestone: + https://github.com/PyCQA/flake8/milestone/52 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 9bf8646..2145179 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -12,6 +12,7 @@ with the newest releases first. 7.0.0 7.1.0 7.1.1 + 7.1.2 6.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 101eafe..0345499 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.1.1" +__version__ = "7.1.2" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From fa2ed7145cdf80fd983c2a436226a074f9a9d664 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Feb 2025 15:21:24 -0500 Subject: [PATCH 236/257] remove a few unnecessary mocks in test_checker_manager noticed while implementing the --jobs limiter --- tests/unit/test_checker_manager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 8d6b3dc..593822b 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -76,9 +76,5 @@ def test_make_checkers(): style_guide = style_guide_mock() style_guide.options.filenames = ["file1", "file2"] manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) - - with mock.patch("flake8.utils.fnmatch", return_value=True): - with mock.patch("flake8.processor.FileProcessor"): - manager.start() - + manager.start() assert manager.filenames == ("file1", "file2") From 9d55ccdb729d1255b9cf09438b6073b05b9ce52c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Mar 2025 15:39:58 -0400 Subject: [PATCH 237/257] py39+ --- .github/workflows/main.yml | 10 +++++----- .pre-commit-config.yaml | 10 +++++----- bin/gen-pycodestyle-plugin | 10 +++++----- docs/source/internal/releases.rst | 4 ++-- docs/source/user/invocation.rst | 10 +++++----- example-plugin/setup.py | 2 -- setup.cfg | 2 +- src/flake8/checker.py | 10 ++++------ src/flake8/discover_files.py | 8 ++++---- src/flake8/main/application.py | 2 +- src/flake8/main/cli.py | 2 +- src/flake8/options/aggregator.py | 2 +- src/flake8/options/manager.py | 2 +- src/flake8/options/parse_args.py | 2 +- src/flake8/plugins/finder.py | 12 ++++++------ src/flake8/plugins/pycodestyle.py | 6 +++--- src/flake8/plugins/pyflakes.py | 4 ++-- src/flake8/processor.py | 16 ++++++---------- src/flake8/statistics.py | 4 ++-- src/flake8/style_guide.py | 18 ++++++------------ src/flake8/utils.py | 4 ++-- src/flake8/violation.py | 2 +- 22 files changed, 64 insertions(+), 78 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e210204..08f54ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,9 +15,6 @@ jobs: - os: ubuntu-latest python: pypy-3.9 toxenv: py - - os: ubuntu-latest - python: 3.8 - toxenv: py - os: ubuntu-latest python: 3.9 toxenv: py @@ -28,11 +25,14 @@ jobs: python: '3.11' toxenv: py - os: ubuntu-latest - python: '3.12-dev' + python: '3.12' + toxenv: py + - os: ubuntu-latest + python: '3.13' toxenv: py # windows - os: windows-latest - python: 3.8 + python: 3.9 toxenv: py # misc - os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad60be1..9df4a79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,19 +12,19 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + rev: v3.14.0 hooks: - id: reorder-python-imports args: [ --application-directories, '.:src', - --py38-plus, + --py39-plus, --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.19.1 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/psf/black rev: 23.12.1 hooks: @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.15.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) diff --git a/bin/gen-pycodestyle-plugin b/bin/gen-pycodestyle-plugin index 8bc2efc..c93fbfe 100755 --- a/bin/gen-pycodestyle-plugin +++ b/bin/gen-pycodestyle-plugin @@ -3,9 +3,9 @@ from __future__ import annotations import inspect import os.path +from collections.abc import Generator from typing import Any from typing import Callable -from typing import Generator from typing import NamedTuple import pycodestyle @@ -42,7 +42,7 @@ class Call(NamedTuple): return cls(func.__name__, inspect.isgeneratorfunction(func), params) -def lines() -> Generator[str, None, None]: +def lines() -> Generator[str]: logical = [] physical = [] @@ -58,8 +58,8 @@ def lines() -> Generator[str, None, None]: yield "# fmt: off" yield "from __future__ import annotations" yield "" + yield "from collections.abc import Generator" yield "from typing import Any" - yield "from typing import Generator" yield "" imports = sorted(call.name for call in logical + physical) for name in imports: @@ -71,7 +71,7 @@ def lines() -> Generator[str, None, None]: logical_params = {param for call in logical for param in call.params} for param in sorted(logical_params): yield f" {param}: Any," - yield ") -> Generator[tuple[int, str], None, None]:" + yield ") -> Generator[tuple[int, str]]:" yield ' """Run pycodestyle logical checks."""' for call in sorted(logical): yield call.to_src() @@ -82,7 +82,7 @@ def lines() -> Generator[str, None, None]: physical_params = {param for call in physical for param in call.params} for param in sorted(physical_params): yield f" {param}: Any," - yield ") -> Generator[tuple[int, str], None, None]:" + yield ") -> Generator[tuple[int, str]]:" yield ' """Run pycodestyle physical checks."""' for call in sorted(physical): yield call.to_src() diff --git a/docs/source/internal/releases.rst b/docs/source/internal/releases.rst index 0081509..d71796d 100644 --- a/docs/source/internal/releases.rst +++ b/docs/source/internal/releases.rst @@ -81,9 +81,9 @@ for users. Before releasing, the following tox test environments must pass: -- Python 3.8 (a.k.a., ``tox -e py38``) +- Python 3.9 (a.k.a., ``tox -e py39``) -- Python 3.12 (a.k.a., ``tox -e py312``) +- Python 3.13 (a.k.a., ``tox -e py313``) - PyPy 3 (a.k.a., ``tox -e pypy3``) diff --git a/docs/source/user/invocation.rst b/docs/source/user/invocation.rst index 61cef97..10895dd 100644 --- a/docs/source/user/invocation.rst +++ b/docs/source/user/invocation.rst @@ -14,25 +14,25 @@ like so: Where you simply allow the shell running in your terminal to locate |Flake8|. In some cases, though, you may have installed |Flake8| for multiple versions -of Python (e.g., Python 3.8 and Python 3.9) and you need to call a specific +of Python (e.g., Python 3.13 and Python 3.14) and you need to call a specific version. In that case, you will have much better results using: .. prompt:: bash - python3.8 -m flake8 + python3.13 -m flake8 Or .. prompt:: bash - python3.9 -m flake8 + python3.14 -m flake8 Since that will tell the correct version of Python to run |Flake8|. .. note:: - Installing |Flake8| once will not install it on both Python 3.8 and - Python 3.9. It will only install it for the version of Python that + Installing |Flake8| once will not install it on both Python 3.13 and + Python 3.14. It will only install it for the version of Python that is running pip. It is also possible to specify command-line options directly to |Flake8|: diff --git a/example-plugin/setup.py b/example-plugin/setup.py index c0720bd..9e7c89f 100644 --- a/example-plugin/setup.py +++ b/example-plugin/setup.py @@ -23,8 +23,6 @@ setuptools.setup( "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", ], diff --git a/setup.cfg b/setup.cfg index 28e6f93..688e349 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.12.0,<2.13.0 pyflakes>=3.2.0,<3.3.0 -python_requires = >=3.8.1 +python_requires = >=3.9 package_dir = =src diff --git a/src/flake8/checker.py b/src/flake8/checker.py index ff62679..d1659b7 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -9,12 +9,10 @@ import multiprocessing.pool import operator import signal import tokenize +from collections.abc import Generator +from collections.abc import Sequence from typing import Any -from typing import Generator -from typing import List from typing import Optional -from typing import Sequence -from typing import Tuple from flake8 import defaults from flake8 import exceptions @@ -27,7 +25,7 @@ from flake8.plugins.finder import Checkers from flake8.plugins.finder import LoadedPlugin from flake8.style_guide import StyleGuideManager -Results = List[Tuple[str, int, int, str, Optional[str]]] +Results = list[tuple[str, int, int, str, Optional[str]]] LOG = logging.getLogger(__name__) @@ -53,7 +51,7 @@ _mp_options: argparse.Namespace @contextlib.contextmanager def _mp_prefork( plugins: Checkers, options: argparse.Namespace -) -> Generator[None, None, None]: +) -> Generator[None]: # we can save significant startup work w/ `fork` multiprocessing global _mp_plugins, _mp_options _mp_plugins, _mp_options = plugins, options diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py index 580d5fd..da28ba5 100644 --- a/src/flake8/discover_files.py +++ b/src/flake8/discover_files.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging import os.path +from collections.abc import Generator +from collections.abc import Sequence from typing import Callable -from typing import Generator -from typing import Sequence from flake8 import utils @@ -16,7 +16,7 @@ def _filenames_from( arg: str, *, predicate: Callable[[str], bool], -) -> Generator[str, None, None]: +) -> Generator[str]: """Generate filenames from an argument. :param arg: @@ -55,7 +55,7 @@ def expand_paths( stdin_display_name: str, filename_patterns: Sequence[str], exclude: Sequence[str], -) -> Generator[str, None, None]: +) -> Generator[str]: """Expand out ``paths`` from commandline to the lintable files.""" if not paths: paths = ["."] diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index b6bfae3..4704cbd 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -5,7 +5,7 @@ import argparse import json import logging import time -from typing import Sequence +from collections.abc import Sequence import flake8 from flake8 import checker diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py index 01a67ac..1a52f36 100644 --- a/src/flake8/main/cli.py +++ b/src/flake8/main/cli.py @@ -2,7 +2,7 @@ from __future__ import annotations import sys -from typing import Sequence +from collections.abc import Sequence from flake8.main import application diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index af8e744..999161a 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -8,7 +8,7 @@ from __future__ import annotations import argparse import configparser import logging -from typing import Sequence +from collections.abc import Sequence from flake8.options import config from flake8.options.manager import OptionManager diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 4fd26b2..cb195fe 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -5,9 +5,9 @@ import argparse import enum import functools import logging +from collections.abc import Sequence from typing import Any from typing import Callable -from typing import Sequence from flake8 import utils from flake8.plugins.finder import Plugins diff --git a/src/flake8/options/parse_args.py b/src/flake8/options/parse_args.py index e3f8795..ff5e08f 100644 --- a/src/flake8/options/parse_args.py +++ b/src/flake8/options/parse_args.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse -from typing import Sequence +from collections.abc import Sequence import flake8 from flake8.main import options diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 380ec3a..88b66a0 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -7,9 +7,9 @@ import inspect import itertools import logging import sys +from collections.abc import Generator +from collections.abc import Iterable from typing import Any -from typing import Generator -from typing import Iterable from typing import NamedTuple from flake8 import utils @@ -68,7 +68,7 @@ class Plugins(NamedTuple): reporters: dict[str, LoadedPlugin] disabled: list[LoadedPlugin] - def all_plugins(self) -> Generator[LoadedPlugin, None, None]: + def all_plugins(self) -> Generator[LoadedPlugin]: """Return an iterator over all :class:`LoadedPlugin`s.""" yield from self.checkers.tree yield from self.checkers.logical_line @@ -151,7 +151,7 @@ def _flake8_plugins( eps: Iterable[importlib.metadata.EntryPoint], name: str, version: str, -) -> Generator[Plugin, None, None]: +) -> Generator[Plugin]: pyflakes_meta = importlib.metadata.distribution("pyflakes").metadata pycodestyle_meta = importlib.metadata.distribution("pycodestyle").metadata @@ -173,7 +173,7 @@ def _flake8_plugins( yield Plugin(name, version, ep) -def _find_importlib_plugins() -> Generator[Plugin, None, None]: +def _find_importlib_plugins() -> Generator[Plugin]: # some misconfigured pythons (RHEL) have things on `sys.path` twice seen = set() for dist in importlib.metadata.distributions(): @@ -212,7 +212,7 @@ def _find_importlib_plugins() -> Generator[Plugin, None, None]: def _find_local_plugins( cfg: configparser.RawConfigParser, -) -> Generator[Plugin, None, None]: +) -> Generator[Plugin]: for plugin_type in ("extension", "report"): group = f"flake8.{plugin_type}" for plugin_s in utils.parse_comma_separated_list( diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py index 9e1d2bb..cd760dc 100644 --- a/src/flake8/plugins/pycodestyle.py +++ b/src/flake8/plugins/pycodestyle.py @@ -2,8 +2,8 @@ # fmt: off from __future__ import annotations +from collections.abc import Generator from typing import Any -from typing import Generator from pycodestyle import ambiguous_identifier as _ambiguous_identifier from pycodestyle import bare_except as _bare_except @@ -55,7 +55,7 @@ def pycodestyle_logical( previous_unindented_logical_line: Any, tokens: Any, verbose: Any, -) -> Generator[tuple[int, str], None, None]: +) -> Generator[tuple[int, str]]: """Run pycodestyle logical checks.""" yield from _ambiguous_identifier(logical_line, tokens) yield from _bare_except(logical_line, noqa) @@ -93,7 +93,7 @@ def pycodestyle_physical( noqa: Any, physical_line: Any, total_lines: Any, -) -> Generator[tuple[int, str], None, None]: +) -> Generator[tuple[int, str]]: """Run pycodestyle physical checks.""" ret = _maximum_line_length(physical_line, max_line_length, multiline, line_number, noqa) # noqa: E501 if ret is not None: diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 6c57619..2835e03 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -4,8 +4,8 @@ from __future__ import annotations import argparse import ast import logging +from collections.abc import Generator from typing import Any -from typing import Generator import pyflakes.checker @@ -97,7 +97,7 @@ class FlakesChecker(pyflakes.checker.Checker): cls.builtIns = cls.builtIns.union(options.builtins) cls.with_doctest = options.doctests - def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]: + def run(self) -> Generator[tuple[int, int, str, type[Any]]]: """Run the plugin.""" for message in self.messages: col = getattr(message, "col", 0) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index e44547b..610964d 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -6,10 +6,8 @@ import ast import functools import logging import tokenize +from collections.abc import Generator from typing import Any -from typing import Generator -from typing import List -from typing import Tuple from flake8 import defaults from flake8 import utils @@ -24,8 +22,8 @@ SKIP_TOKENS = frozenset( [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT] ) -_LogicalMapping = List[Tuple[int, Tuple[int, int]]] -_Logical = Tuple[List[str], List[str], _LogicalMapping] +_LogicalMapping = list[tuple[int, tuple[int, int]]] +_Logical = tuple[list[str], list[str], _LogicalMapping] class FileProcessor: @@ -127,9 +125,7 @@ class FileProcessor: """Signal the beginning of an fstring.""" self._fstring_start = lineno - def multiline_string( - self, token: tokenize.TokenInfo - ) -> Generator[str, None, None]: + def multiline_string(self, token: tokenize.TokenInfo) -> Generator[str]: """Iterate through the lines of a multiline string.""" if token.type == FSTRING_END: # pragma: >=3.12 cover start = self._fstring_start @@ -210,7 +206,7 @@ class FileProcessor: brace_offset = text.count("{") + text.count("}") text = "x" * (len(text) + brace_offset) end = (end[0], end[1] + brace_offset) - if previous_row: + if previous_row is not None and previous_column is not None: (start_row, start_column) = start if previous_row != start_row: row_index = previous_row - 1 @@ -263,7 +259,7 @@ class FileProcessor: ) return ret - def generate_tokens(self) -> Generator[tokenize.TokenInfo, None, None]: + def generate_tokens(self) -> Generator[tokenize.TokenInfo]: """Tokenize the file and yield the tokens.""" for token in tokenize.generate_tokens(self.next_line): if token[2][0] > self.total_lines: diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index a33e6a6..5a22254 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -1,7 +1,7 @@ """Statistic collection logic for Flake8.""" from __future__ import annotations -from typing import Generator +from collections.abc import Generator from typing import NamedTuple from flake8.violation import Violation @@ -36,7 +36,7 @@ class Statistics: def statistics_for( self, prefix: str, filename: str | None = None - ) -> Generator[Statistic, None, None]: + ) -> Generator[Statistic]: """Generate statistics for the prefix and filename. If you have a :class:`Statistics` object that has recorded errors, diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index a409484..f72e6d8 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -7,8 +7,8 @@ import copy import enum import functools import logging -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from flake8 import defaults from flake8 import statistics @@ -225,13 +225,11 @@ class StyleGuideManager: *self.populate_style_guides_with(options), ] - self.style_guide_for = functools.lru_cache(maxsize=None)( - self._style_guide_for - ) + self.style_guide_for = functools.cache(self._style_guide_for) def populate_style_guides_with( self, options: argparse.Namespace - ) -> Generator[StyleGuide, None, None]: + ) -> Generator[StyleGuide]: """Generate style guides from the per-file-ignores option. :param options: @@ -253,9 +251,7 @@ class StyleGuideManager: ) @contextlib.contextmanager - def processing_file( - self, filename: str - ) -> Generator[StyleGuide, None, None]: + def processing_file(self, filename: str) -> Generator[StyleGuide]: """Record the fact that we're processing the file's results.""" guide = self.style_guide_for(filename) with guide.processing_file(filename): @@ -338,9 +334,7 @@ class StyleGuide: ) @contextlib.contextmanager - def processing_file( - self, filename: str - ) -> Generator[StyleGuide, None, None]: + def processing_file(self, filename: str) -> Generator[StyleGuide]: """Record the fact that we're processing the file's results.""" self.formatter.beginning(filename) yield self diff --git a/src/flake8/utils.py b/src/flake8/utils.py index afc3896..67db33f 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -11,9 +11,9 @@ import re import sys import textwrap import tokenize +from collections.abc import Sequence +from re import Pattern from typing import NamedTuple -from typing import Pattern -from typing import Sequence from flake8 import exceptions diff --git a/src/flake8/violation.py b/src/flake8/violation.py index 96161d4..ae1631a 100644 --- a/src/flake8/violation.py +++ b/src/flake8/violation.py @@ -4,7 +4,7 @@ from __future__ import annotations import functools import linecache import logging -from typing import Match +from re import Match from typing import NamedTuple from flake8 import defaults From d56d569ce40a623a17c212ea7f2b306714f27f31 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Mar 2025 15:53:41 -0400 Subject: [PATCH 238/257] update versions of pycodestyle / pyflakes --- setup.cfg | 4 ++-- src/flake8/plugins/pyflakes.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 688e349..6f63f5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,8 +29,8 @@ classifiers = packages = find: install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.12.0,<2.13.0 - pyflakes>=3.2.0,<3.3.0 + pycodestyle>=2.13.0,<2.14.0 + pyflakes>=3.3.0,<3.4.0 python_requires = >=3.9 package_dir = =src diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 2835e03..3620a27 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -55,6 +55,7 @@ FLAKE8_PYFLAKES_CODES = { "UndefinedName": "F821", "UndefinedExport": "F822", "UndefinedLocal": "F823", + "UnusedIndirectAssignment": "F824", "DuplicateArgument": "F831", "UnusedVariable": "F841", "UnusedAnnotation": "F842", From 16f5f28a384f0781bebb37a08aa45e65b9526c50 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Mar 2025 16:17:35 -0400 Subject: [PATCH 239/257] Release 7.2.0 --- docs/source/release-notes/7.2.0.rst | 19 +++++++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/7.2.0.rst diff --git a/docs/source/release-notes/7.2.0.rst b/docs/source/release-notes/7.2.0.rst new file mode 100644 index 0000000..fe124d7 --- /dev/null +++ b/docs/source/release-notes/7.2.0.rst @@ -0,0 +1,19 @@ +7.2.0 -- 2025-03-29 +------------------- + +You can view the `7.2.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.13.0, < 2.14.0 (See also :pull:`1974`). +- pyflakes has been updated to >= 3.3.0, < 3.4.0 (See also :pull:`1974`). + +Features +~~~~~~~~ + +- Require python >= 3.9 (See also :pull:`1973`). + +.. all links +.. _7.2.0 milestone: + https://github.com/PyCQA/flake8/milestone/53 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 2145179..a4d8bfc 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -13,6 +13,7 @@ with the newest releases first. 7.1.0 7.1.1 7.1.2 + 7.2.0 6.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 0345499..cf91f8b 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.1.2" +__version__ = "7.2.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 3613896bd9051147ffa7fd04ac1a98cbc9e35cf2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Mar 2025 10:05:31 -0400 Subject: [PATCH 240/257] document F824 --- docs/source/user/error-codes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/user/error-codes.rst b/docs/source/user/error-codes.rst index 2a91413..3090d47 100644 --- a/docs/source/user/error-codes.rst +++ b/docs/source/user/error-codes.rst @@ -102,6 +102,9 @@ generates its own :term:`error code`\ s for ``pyflakes``: +------+---------------------------------------------------------------------+ | F823 | local variable ``name`` ... referenced before assignment | +------+---------------------------------------------------------------------+ +| F824 | ``global name`` / ``nonlocal name`` is unused: name is never | +| | assigned in scope | ++------+---------------------------------------------------------------------+ | F831 | duplicate argument ``name`` in function definition | +------+---------------------------------------------------------------------+ | F841 | local variable ``name`` is assigned to but never used | From 8dfa6695b4fb1e1401b357367a0a71037d29f6aa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 11 Apr 2025 17:39:39 -0400 Subject: [PATCH 241/257] add rtd sphinx config --- .readthedocs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0425dc2..dfa8b9d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,3 +8,5 @@ python: install: - path: . - requirements: docs/source/requirements.txt +sphinx: + configuration: docs/source/conf.py From 019424b80d3d7d5d8a2a1638f5877080546e3f46 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 23 May 2025 16:25:06 -0400 Subject: [PATCH 242/257] add support for t-strings --- src/flake8/_compat.py | 7 +++++++ src/flake8/checker.py | 3 +++ src/flake8/processor.py | 17 +++++++++++++--- tests/integration/test_plugins.py | 33 +++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py index e8a3ccd..22bb84e 100644 --- a/src/flake8/_compat.py +++ b/src/flake8/_compat.py @@ -9,3 +9,10 @@ if sys.version_info >= (3, 12): # pragma: >=3.12 cover FSTRING_END = tokenize.FSTRING_END else: # pragma: <3.12 cover FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1 + +if sys.version_info >= (3, 14): # pragma: >=3.14 cover + TSTRING_START = tokenize.TSTRING_START + TSTRING_MIDDLE = tokenize.TSTRING_MIDDLE + TSTRING_END = tokenize.TSTRING_END +else: # pragma: <3.14 cover + TSTRING_START = TSTRING_MIDDLE = TSTRING_END = -1 diff --git a/src/flake8/checker.py b/src/flake8/checker.py index d1659b7..84d45aa 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -19,6 +19,7 @@ from flake8 import exceptions from flake8 import processor from flake8 import utils from flake8._compat import FSTRING_START +from flake8._compat import TSTRING_START from flake8.discover_files import expand_paths from flake8.options.parse_args import parse_args from flake8.plugins.finder import Checkers @@ -554,6 +555,8 @@ class FileChecker: assert self.processor is not None if token.type == FSTRING_START: # pragma: >=3.12 cover self.processor.fstring_start(token.start[0]) + elif token.type == TSTRING_START: # pragma: >=3.14 cover + self.processor.tstring_start(token.start[0]) # a newline token ends a single physical line. elif processor.is_eol_token(token): # if the file does not end with a newline, the NEWLINE diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 610964d..ccb4c57 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -13,6 +13,8 @@ from flake8 import defaults from flake8 import utils from flake8._compat import FSTRING_END from flake8._compat import FSTRING_MIDDLE +from flake8._compat import TSTRING_END +from flake8._compat import TSTRING_MIDDLE from flake8.plugins.finder import LoadedPlugin LOG = logging.getLogger(__name__) @@ -113,7 +115,7 @@ class FileProcessor: self.verbose = options.verbose #: Statistics dictionary self.statistics = {"logical lines": 0} - self._fstring_start = -1 + self._fstring_start = self._tstring_start = -1 @functools.cached_property def file_tokens(self) -> list[tokenize.TokenInfo]: @@ -125,10 +127,16 @@ class FileProcessor: """Signal the beginning of an fstring.""" self._fstring_start = lineno + def tstring_start(self, lineno: int) -> None: # pragma: >=3.14 cover + """Signal the beginning of an tstring.""" + self._tstring_start = lineno + def multiline_string(self, token: tokenize.TokenInfo) -> Generator[str]: """Iterate through the lines of a multiline string.""" if token.type == FSTRING_END: # pragma: >=3.12 cover start = self._fstring_start + elif token.type == TSTRING_END: # pragma: >=3.14 cover + start = self._tstring_start else: start = token.start[0] @@ -198,7 +206,10 @@ class FileProcessor: continue if token_type == tokenize.STRING: text = mutate_string(text) - elif token_type == FSTRING_MIDDLE: # pragma: >=3.12 cover + elif token_type in { + FSTRING_MIDDLE, + TSTRING_MIDDLE, + }: # pragma: >=3.12 cover # noqa: E501 # A curly brace in an FSTRING_MIDDLE token must be an escaped # curly brace. Both 'text' and 'end' will account for the # escaped version of the token (i.e. a single brace) rather @@ -382,7 +393,7 @@ def is_eol_token(token: tokenize.TokenInfo) -> bool: def is_multiline_string(token: tokenize.TokenInfo) -> bool: """Check if this is a multiline string.""" - return token.type == FSTRING_END or ( + return token.type in {FSTRING_END, TSTRING_END} or ( token.type == tokenize.STRING and "\n" in token.string ) diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 90ca555..471cab8 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -296,3 +296,36 @@ t.py:1:1: T001 "f'xxxxxxxxxxxxxxxxxxxxxxxx'" """ out, err = capsys.readouterr() assert out == expected + + +@pytest.mark.xfail(sys.version_info < (3, 14), reason="3.14+") +def test_tstring_logical_line(tmpdir, capsys): # pragma: >=3.14 cover + cfg_s = f"""\ +[flake8] +extend-ignore = F +[flake8:local-plugins] +extension = + T = {yields_logical_line.__module__}:{yields_logical_line.__name__} +""" + + cfg = tmpdir.join("tox.ini") + cfg.write(cfg_s) + + src = """\ +t''' +hello {world} +''' +t'{{"{hello}": "{world}"}}' +""" + t_py = tmpdir.join("t.py") + t_py.write_binary(src.encode()) + + with tmpdir.as_cwd(): + assert main(("t.py", "--config", str(cfg))) == 1 + + expected = """\ +t.py:1:1: T001 "t'''xxxxxxx{world}x'''" +t.py:4:1: T001 "t'xxx{hello}xxxx{world}xxx'" +""" + out, err = capsys.readouterr() + assert out == expected From 4941a3e32e54488698ecbc23993bfeb2a60c0fc5 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 20 Jun 2025 15:15:53 -0400 Subject: [PATCH 243/257] upgrade pyflakes / pycodestyle --- setup.cfg | 4 ++-- src/flake8/plugins/pyflakes.py | 1 + tests/unit/test_checker_manager.py | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6f63f5a..a6b5a5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,8 +29,8 @@ classifiers = packages = find: install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.13.0,<2.14.0 - pyflakes>=3.3.0,<3.4.0 + pycodestyle>=2.14.0,<2.15.0 + pyflakes>=3.4.0,<3.5.0 python_requires = >=3.9 package_dir = =src diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 3620a27..66d8c1c 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -36,6 +36,7 @@ FLAKE8_PYFLAKES_CODES = { "StringDotFormatMissingArgument": "F524", "StringDotFormatMixingAutomatic": "F525", "FStringMissingPlaceholders": "F541", + "TStringMissingPlaceholders": "F542", "MultiValueRepeatedKeyLiteral": "F601", "MultiValueRepeatedKeyVariable": "F602", "TooManyExpressionsInStarredAssignment": "F621", diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 593822b..eecba3b 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -41,9 +41,11 @@ def test_oserrors_are_reraised(): err = OSError(errno.EAGAIN, "Ominous message") with mock.patch("_multiprocessing.SemLock", side_effect=err): manager = _parallel_checker_manager() - with mock.patch.object(manager, "run_serial") as serial: - with pytest.raises(OSError): - manager.run() + with ( + mock.patch.object(manager, "run_serial") as serial, + pytest.raises(OSError), + ): + manager.run() assert serial.call_count == 0 From 6bcdb628597fa2d03494965089ff87a492ffc1e9 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 20 Jun 2025 15:21:27 -0400 Subject: [PATCH 244/257] document F542 --- docs/source/user/error-codes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/user/error-codes.rst b/docs/source/user/error-codes.rst index 3090d47..c8b46c1 100644 --- a/docs/source/user/error-codes.rst +++ b/docs/source/user/error-codes.rst @@ -59,6 +59,8 @@ generates its own :term:`error code`\ s for ``pyflakes``: +------+---------------------------------------------------------------------+ | F541 | f-string without any placeholders | +------+---------------------------------------------------------------------+ +| F542 | t-string without any placeholders | ++------+---------------------------------------------------------------------+ +------+---------------------------------------------------------------------+ | F601 | dictionary key ``name`` repeated with different values | +------+---------------------------------------------------------------------+ From c48217e1fc006c2dddd14df54e83b67da15de5cd Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 20 Jun 2025 15:30:19 -0400 Subject: [PATCH 245/257] Release 7.3.0 --- docs/source/release-notes/7.3.0.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 11 ++++++----- src/flake8/__init__.py | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 docs/source/release-notes/7.3.0.rst diff --git a/docs/source/release-notes/7.3.0.rst b/docs/source/release-notes/7.3.0.rst new file mode 100644 index 0000000..dedc918 --- /dev/null +++ b/docs/source/release-notes/7.3.0.rst @@ -0,0 +1,15 @@ +7.3.0 -- 2025-06-20 +------------------- + +You can view the `7.3.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Added support for python 3.14 (See also :pull:`1983`). +- pycodestyle has been updated to >= 2.14.0, < 2.15.0 (See also :pull:`1985`). +- Pyflakes has been updated to >= 3.4.0, < 3.5.0 (See also :pull:`1985`). + +.. all links +.. _7.3.0 milestone: + https://github.com/PyCQA/flake8/milestone/54 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index a4d8bfc..10697df 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -9,18 +9,19 @@ with the newest releases first. ================== .. toctree:: - 7.0.0 - 7.1.0 - 7.1.1 - 7.1.2 + 7.3.0 7.2.0 + 7.1.2 + 7.1.1 + 7.1.0 + 7.0.0 6.x Release Series ================== .. toctree:: - 6.0.0 6.1.0 + 6.0.0 5.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index cf91f8b..db29166 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.2.0" +__version__ = "7.3.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 628aece714c9265e8def265f5fcc574605aca524 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 20 Jun 2025 15:40:43 -0400 Subject: [PATCH 246/257] adjust global variable definition for new pyflakes the original code was only passing pyflakes by accident due to __future__.annotations --- src/flake8/checker.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 84d45aa..c52955d 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -45,8 +45,7 @@ SERIAL_RETRY_ERRNOS = { # noise in diffs. } -_mp_plugins: Checkers -_mp_options: argparse.Namespace +_mp: tuple[Checkers, argparse.Namespace] | None = None @contextlib.contextmanager @@ -54,31 +53,31 @@ def _mp_prefork( plugins: Checkers, options: argparse.Namespace ) -> Generator[None]: # we can save significant startup work w/ `fork` multiprocessing - global _mp_plugins, _mp_options - _mp_plugins, _mp_options = plugins, options + global _mp + _mp = plugins, options try: yield finally: - del _mp_plugins, _mp_options + _mp = None def _mp_init(argv: Sequence[str]) -> None: - global _mp_plugins, _mp_options + global _mp # Ensure correct signaling of ^C using multiprocessing.Pool. signal.signal(signal.SIGINT, signal.SIG_IGN) - try: - # for `fork` this'll already be set - _mp_plugins, _mp_options # noqa: B018 - except NameError: + # for `fork` this'll already be set + if _mp is None: plugins, options = parse_args(argv) - _mp_plugins, _mp_options = plugins.checkers, options + _mp = plugins.checkers, options def _mp_run(filename: str) -> tuple[str, Results, dict[str, int]]: + assert _mp is not None, _mp + plugins, options = _mp return FileChecker( - filename=filename, plugins=_mp_plugins, options=_mp_options + filename=filename, plugins=plugins, options=options ).run_checks() From 5fab0d18870b861ffbcb2757adb44154aff126c2 Mon Sep 17 00:00:00 2001 From: Max R Date: Sun, 20 Jul 2025 19:12:27 -0400 Subject: [PATCH 247/257] Update hooks and use `autopep8` + `add-trailing-comma` instead of `black` --- .pre-commit-config.yaml | 23 ++++++++++++--------- setup.cfg | 1 - src/flake8/__init__.py | 2 +- src/flake8/api/legacy.py | 4 ++-- src/flake8/checker.py | 16 +++++++-------- src/flake8/formatting/base.py | 2 +- src/flake8/main/application.py | 2 +- src/flake8/main/debug.py | 2 +- src/flake8/main/options.py | 2 +- src/flake8/options/config.py | 6 +++--- src/flake8/options/manager.py | 2 +- src/flake8/plugins/finder.py | 10 ++++----- src/flake8/plugins/pyflakes.py | 2 +- src/flake8/processor.py | 10 ++++----- src/flake8/statistics.py | 4 ++-- src/flake8/style_guide.py | 10 ++++----- src/flake8/utils.py | 6 +++--- src/flake8/violation.py | 2 +- tests/integration/test_checker.py | 6 +++--- tests/integration/test_main.py | 6 +++--- tests/integration/test_plugins.py | 2 +- tests/unit/plugins/finder_test.py | 32 +++++++++++++++++------------ tests/unit/plugins/reporter_test.py | 4 ++-- tests/unit/test_application.py | 2 +- tests/unit/test_base_formatter.py | 6 +++--- tests/unit/test_debug.py | 2 +- tests/unit/test_decision_engine.py | 10 ++++----- tests/unit/test_discover_files.py | 6 +++--- tests/unit/test_file_processor.py | 12 +++++------ tests/unit/test_main_options.py | 2 +- tests/unit/test_option_manager.py | 8 ++++---- tests/unit/test_options_config.py | 4 ++-- tests/unit/test_style_guide.py | 4 ++-- 33 files changed, 110 insertions(+), 102 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9df4a79..837e6e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,10 @@ repos: +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.2.0 + hooks: + - id: add-trailing-comma - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: check-yaml - id: debug-statements @@ -8,11 +12,11 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.5.0 + rev: v2.8.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.14.0 + rev: v3.15.0 hooks: - id: reorder-python-imports args: [ @@ -21,21 +25,20 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade args: [--py39-plus] -- repo: https://github.com/psf/black - rev: 23.12.1 +- repo: https://github.com/hhatto/autopep8 + rev: v2.3.2 hooks: - - id: black - args: [--line-length=79] + - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.17.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) diff --git a/setup.cfg b/setup.cfg index a6b5a5e..dc967d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,6 @@ classifiers = Environment :: Console Framework :: Flake8 Intended Audience :: Developers - License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index db29166..0dea638 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -66,5 +66,5 @@ def configure_logging( LOG.addHandler(handler) LOG.setLevel(log_level) LOG.debug( - "Added a %s logging handler to logger root at %s", filename, __name__ + "Added a %s logging handler to logger root at %s", filename, __name__, ) diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 446df29..4d5c91d 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -135,7 +135,7 @@ class StyleGuide: stdin_display_name=self.options.stdin_display_name, filename_patterns=self.options.filename, exclude=self.options.exclude, - ) + ), ) return not paths @@ -153,7 +153,7 @@ class StyleGuide: if not issubclass(reporter, formatter.BaseFormatter): raise ValueError( "Report should be subclass of " - "flake8.formatter.BaseFormatter." + "flake8.formatter.BaseFormatter.", ) self._application.formatter = reporter(self.options) self._application.guide = None diff --git a/src/flake8/checker.py b/src/flake8/checker.py index c52955d..d957915 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -50,7 +50,7 @@ _mp: tuple[Checkers, argparse.Namespace] | None = None @contextlib.contextmanager def _mp_prefork( - plugins: Checkers, options: argparse.Namespace + plugins: Checkers, options: argparse.Namespace, ) -> Generator[None]: # we can save significant startup work w/ `fork` multiprocessing global _mp @@ -77,7 +77,7 @@ def _mp_run(filename: str) -> tuple[str, Results, dict[str, int]]: assert _mp is not None, _mp plugins, options = _mp return FileChecker( - filename=filename, plugins=plugins, options=options + filename=filename, plugins=plugins, options=options, ).run_checks() @@ -137,7 +137,7 @@ class Manager: if utils.is_using_stdin(self.options.filenames): LOG.warning( "The --jobs option is not compatible with supplying " - "input using - . Ignoring --jobs arguments." + "input using - . Ignoring --jobs arguments.", ) return 0 @@ -252,7 +252,7 @@ class Manager: stdin_display_name=self.options.stdin_display_name, filename_patterns=self.options.filename, exclude=self.exclude, - ) + ), ) self.jobs = min(len(self.filenames), self.jobs) @@ -332,11 +332,11 @@ class FileChecker: assert self.processor is not None, self.filename try: params = self.processor.keyword_arguments_for( - plugin.parameters, arguments + plugin.parameters, arguments, ) except AttributeError as ae: raise exceptions.PluginRequestedUnknownParameters( - plugin_name=plugin.display_name, exception=ae + plugin_name=plugin.display_name, exception=ae, ) try: return plugin.obj(**arguments, **params) @@ -548,7 +548,7 @@ class FileChecker: self.run_logical_checks() def check_physical_eol( - self, token: tokenize.TokenInfo, prev_physical: str + self, token: tokenize.TokenInfo, prev_physical: str, ) -> None: """Run physical checks if and only if it is at the end of the line.""" assert self.processor is not None @@ -598,7 +598,7 @@ def _try_initialize_processpool( def find_offset( - offset: int, mapping: processor._LogicalMapping + offset: int, mapping: processor._LogicalMapping, ) -> tuple[int, int]: """Find the offset tuple for a single offset.""" if isinstance(offset, tuple): diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index d986d65..bbbfdff 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -110,7 +110,7 @@ class BaseFormatter: The formatted error string. """ raise NotImplementedError( - "Subclass of BaseFormatter did not implement" " format." + "Subclass of BaseFormatter did not implement" " format.", ) def show_statistics(self, statistics: Statistics) -> None: diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 4704cbd..165a6ef 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -76,7 +76,7 @@ class Application: assert self.formatter is not None assert self.options is not None self.guide = style_guide.StyleGuideManager( - self.options, self.formatter + self.options, self.formatter, ) def make_file_checker_manager(self, argv: Sequence[str]) -> None: diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index c3a8b0b..73ca74b 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -14,7 +14,7 @@ def information(version: str, plugins: Plugins) -> dict[str, Any]: (loaded.plugin.package, loaded.plugin.version) for loaded in plugins.all_plugins() if loaded.plugin.package not in {"flake8", "local"} - } + }, ) return { "version": version, diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index 9d57321..e8cbe09 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -32,7 +32,7 @@ def stage1_arg_parser() -> argparse.ArgumentParser: ) parser.add_argument( - "--output-file", default=None, help="Redirect report to a file." + "--output-file", default=None, help="Redirect report to a file.", ) # Config file options diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index b51949c..fddee55 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -78,7 +78,7 @@ def load_config( if config is not None: if not cfg.read(config, encoding="UTF-8"): raise exceptions.ExecutionError( - f"The specified config file does not exist: {config}" + f"The specified config file does not exist: {config}", ) cfg_dir = os.path.dirname(config) else: @@ -89,7 +89,7 @@ def load_config( for filename in extra: if not cfg.read(filename, encoding="UTF-8"): raise exceptions.ExecutionError( - f"The specified config file does not exist: {filename}" + f"The specified config file does not exist: {filename}", ) return cfg, cfg_dir @@ -131,7 +131,7 @@ def parse_config( raise ValueError( f"Error code {error_code!r} " f"supplied to {option_name!r} option " - f"does not match {VALID_CODE_PREFIX.pattern!r}" + f"does not match {VALID_CODE_PREFIX.pattern!r}", ) assert option.config_name is not None diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index cb195fe..addd3ec 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -165,7 +165,7 @@ class Option: if long_option_name is _ARG.NO: raise ValueError( "When specifying parse_from_config=True, " - "a long_option_name must also be specified." + "a long_option_name must also be specified.", ) self.config_name = long_option_name[2:].replace("-", "_") diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 88b66a0..4da3402 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -83,8 +83,8 @@ class Plugins(NamedTuple): f"{loaded.plugin.package}: {loaded.plugin.version}" for loaded in self.all_plugins() if loaded.plugin.package not in {"flake8", "local"} - } - ) + }, + ), ) @@ -167,7 +167,7 @@ def _flake8_plugins( # ideally pycodestyle's plugin entrypoints would exactly represent # the codes they produce... yield Plugin( - pycodestyle_meta["name"], pycodestyle_meta["version"], ep + pycodestyle_meta["name"], pycodestyle_meta["version"], ep, ) else: yield Plugin(name, version, ep) @@ -240,7 +240,7 @@ def _check_required_plugins( f"required plugins were not installed!\n" f"- installed: {', '.join(sorted(plugin_names))}\n" f"- expected: {', '.join(sorted(expected_names))}\n" - f"- missing: {', '.join(sorted(missing_plugins))}" + f"- missing: {', '.join(sorted(missing_plugins))}", ) @@ -338,7 +338,7 @@ def _classify_plugins( if not VALID_CODE_PREFIX.match(loaded.entry_name): raise ExecutionError( f"plugin code for `{loaded.display_name}` does not match " - f"{VALID_CODE_PREFIX.pattern}" + f"{VALID_CODE_PREFIX.pattern}", ) return Plugins( diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 66d8c1c..9844025 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -72,7 +72,7 @@ class FlakesChecker(pyflakes.checker.Checker): def __init__(self, tree: ast.AST, filename: str) -> None: """Initialize the PyFlakes plugin with an AST tree and filename.""" super().__init__( - tree, filename=filename, withDoctest=self.with_doctest + tree, filename=filename, withDoctest=self.with_doctest, ) @classmethod diff --git a/src/flake8/processor.py b/src/flake8/processor.py index ccb4c57..b1742ca 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -21,7 +21,7 @@ LOG = logging.getLogger(__name__) NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE]) SKIP_TOKENS = frozenset( - [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT] + [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT], ) _LogicalMapping = list[tuple[int, tuple[int, int]]] @@ -173,7 +173,7 @@ class FileProcessor: """Update the checker_state attribute for the plugin.""" if "checker_state" in plugin.parameters: self.checker_state = self._checker_states.setdefault( - plugin.entry_name, {} + plugin.entry_name, {}, ) def next_logical_line(self) -> None: @@ -280,7 +280,7 @@ class FileProcessor: def _noqa_line_range(self, min_line: int, max_line: int) -> dict[int, str]: line_range = range(min_line, max_line + 1) - joined = "".join(self.lines[min_line - 1 : max_line]) + joined = "".join(self.lines[min_line - 1: max_line]) return dict.fromkeys(line_range, joined) @functools.cached_property @@ -367,7 +367,7 @@ class FileProcessor: elif any(defaults.NOQA_FILE.search(line) for line in self.lines): LOG.warning( "Detected `flake8: noqa` on line with code. To ignore an " - "error on a line use `noqa` instead." + "error on a line use `noqa` instead.", ) return False else: @@ -388,7 +388,7 @@ class FileProcessor: def is_eol_token(token: tokenize.TokenInfo) -> bool: """Check if the token is an end-of-line token.""" - return token[0] in NEWLINE or token[4][token[3][1] :].lstrip() == "\\\n" + return token[0] in NEWLINE or token[4][token[3][1]:].lstrip() == "\\\n" def is_multiline_string(token: tokenize.TokenInfo) -> bool: diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index 5a22254..b30e4c7 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -35,7 +35,7 @@ class Statistics: self._store[key].increment() def statistics_for( - self, prefix: str, filename: str | None = None + self, prefix: str, filename: str | None = None, ) -> Generator[Statistic]: """Generate statistics for the prefix and filename. @@ -108,7 +108,7 @@ class Statistic: """ def __init__( - self, error_code: str, filename: str, message: str, count: int + self, error_code: str, filename: str, message: str, count: int, ) -> None: """Initialize our Statistic.""" self.error_code = error_code diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index f72e6d8..d675df7 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -218,7 +218,7 @@ class StyleGuideManager: self.decider = decider or DecisionEngine(options) self.style_guides: list[StyleGuide] = [] self.default_style_guide = StyleGuide( - options, formatter, self.stats, decider=decider + options, formatter, self.stats, decider=decider, ) self.style_guides = [ self.default_style_guide, @@ -228,7 +228,7 @@ class StyleGuideManager: self.style_guide_for = functools.cache(self._style_guide_for) def populate_style_guides_with( - self, options: argparse.Namespace + self, options: argparse.Namespace, ) -> Generator[StyleGuide]: """Generate style guides from the per-file-ignores option. @@ -240,7 +240,7 @@ class StyleGuideManager: per_file = utils.parse_files_to_codes_mapping(options.per_file_ignores) for filename, violations in per_file: yield self.default_style_guide.copy( - filename=filename, extend_ignore_with=violations + filename=filename, extend_ignore_with=violations, ) def _style_guide_for(self, filename: str) -> StyleGuide: @@ -288,7 +288,7 @@ class StyleGuideManager: """ guide = self.style_guide_for(filename) return guide.handle_error( - code, filename, line_number, column_number, text, physical_line + code, filename, line_number, column_number, text, physical_line, ) @@ -330,7 +330,7 @@ class StyleGuide: options.extend_ignore = options.extend_ignore or [] options.extend_ignore.extend(extend_ignore_with or []) return StyleGuide( - options, self.formatter, self.stats, filename=filename + options, self.formatter, self.stats, filename=filename, ) @contextlib.contextmanager diff --git a/src/flake8/utils.py b/src/flake8/utils.py index 67db33f..e5c086e 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -23,7 +23,7 @@ NORMALIZE_PACKAGE_NAME_RE = re.compile(r"[-_.]+") def parse_comma_separated_list( - value: str, regexp: Pattern[str] = COMMA_SEPARATED_LIST_RE + value: str, regexp: Pattern[str] = COMMA_SEPARATED_LIST_RE, ) -> list[str]: """Parse a comma-separated list. @@ -115,7 +115,7 @@ def parse_files_to_codes_mapping( # noqa: C901 f"Expected `per-file-ignores` to be a mapping from file exclude " f"patterns to ignore codes.\n\n" f"Configured `per-file-ignores` setting:\n\n" - f"{textwrap.indent(value.strip(), ' ')}" + f"{textwrap.indent(value.strip(), ' ')}", ) for token in _tokenize_files_to_codes_mapping(value): @@ -150,7 +150,7 @@ def parse_files_to_codes_mapping( # noqa: C901 def normalize_paths( - paths: Sequence[str], parent: str = os.curdir + paths: Sequence[str], parent: str = os.curdir, ) -> list[str]: """Normalize a list of paths relative to a parent directory. diff --git a/src/flake8/violation.py b/src/flake8/violation.py index ae1631a..8535178 100644 --- a/src/flake8/violation.py +++ b/src/flake8/violation.py @@ -64,6 +64,6 @@ class Violation(NamedTuple): return True LOG.debug( - "%r is not ignored inline with ``# noqa: %s``", self, codes_str + "%r is not ignored inline with ``# noqa: %s``", self, codes_str, ) return False diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index a585f5a..60d0132 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -97,7 +97,7 @@ def mock_file_checker_with_plugin(plugin_target): # Prevent it from reading lines from stdin or somewhere else with mock.patch( - "flake8.processor.FileProcessor.read_lines", return_value=["Line 1"] + "flake8.processor.FileProcessor.read_lines", return_value=["Line 1"], ): file_checker = checker.FileChecker( filename="-", @@ -325,12 +325,12 @@ def test_handling_syntaxerrors_across_pythons(): if sys.version_info < (3, 10): # pragma: no cover (<3.10) # Python 3.9 or older err = SyntaxError( - "invalid syntax", ("", 2, 5, "bad python:\n") + "invalid syntax", ("", 2, 5, "bad python:\n"), ) expected = (2, 4) else: # pragma: no cover (3.10+) err = SyntaxError( - "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11) + "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11), ) expected = (2, 1) file_checker = checker.FileChecker( diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 68b93cb..5091573 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -314,7 +314,7 @@ def test_cli_config_option_respected(tmp_path): """\ [flake8] ignore = F401 -""" +""", ) py_file = tmp_path / "t.py" @@ -330,7 +330,7 @@ def test_cli_isolated_overrides_config_option(tmp_path): """\ [flake8] ignore = F401 -""" +""", ) py_file = tmp_path / "t.py" @@ -364,7 +364,7 @@ def test_output_file(tmpdir, capsys): def test_early_keyboard_interrupt_does_not_crash(capsys): with mock.patch.object( - config, "load_config", side_effect=KeyboardInterrupt + config, "load_config", side_effect=KeyboardInterrupt, ): assert cli.main(["does-not-exist"]) == 1 out, err = capsys.readouterr() diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 471cab8..9f9c1a0 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -86,7 +86,7 @@ def test_local_plugin_can_add_option(local_config): stage1_args, rest = stage1_parser.parse_known_args(argv) cfg, cfg_dir = config.load_config( - config=stage1_args.config, extra=[], isolated=False + config=stage1_args.config, extra=[], isolated=False, ) opts = finder.parse_plugin_options( diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index b289bef..a155ef1 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -42,7 +42,7 @@ def test_plugins_all_plugins(): logical_line_plugin = _loaded(parameters={"logical_line": True}) physical_line_plugin = _loaded(parameters={"physical_line": True}) report_plugin = _loaded( - plugin=_plugin(ep=_ep(name="R", group="flake8.report")) + plugin=_plugin(ep=_ep(name="R", group="flake8.report")), ) plugins = finder.Plugins( @@ -200,14 +200,16 @@ def test_flake8_plugins(flake8_dist, mock_distribution): "flake8", "9001", importlib.metadata.EntryPoint( - "default", "flake8.formatting.default:Default", "flake8.report" + "default", + "flake8.formatting.default:Default", + "flake8.report", ), ), finder.Plugin( "flake8", "9001", importlib.metadata.EntryPoint( - "pylint", "flake8.formatting.default:Pylint", "flake8.report" + "pylint", "flake8.formatting.default:Pylint", "flake8.report", ), ), } @@ -270,7 +272,7 @@ unrelated = unrelated:main "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "Q", "flake8_foo:Plugin", "flake8.extension" + "Q", "flake8_foo:Plugin", "flake8.extension", ), ), finder.Plugin( @@ -304,21 +306,23 @@ unrelated = unrelated:main "flake8", "9001", importlib.metadata.EntryPoint( - "default", "flake8.formatting.default:Default", "flake8.report" + "default", + "flake8.formatting.default:Default", + "flake8.report", ), ), finder.Plugin( "flake8", "9001", importlib.metadata.EntryPoint( - "pylint", "flake8.formatting.default:Pylint", "flake8.report" + "pylint", "flake8.formatting.default:Pylint", "flake8.report", ), ), finder.Plugin( "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "foo", "flake8_foo:Formatter", "flake8.report" + "foo", "flake8_foo:Formatter", "flake8.report", ), ), } @@ -485,28 +489,30 @@ def test_find_plugins( "flake8", "9001", importlib.metadata.EntryPoint( - "default", "flake8.formatting.default:Default", "flake8.report" + "default", + "flake8.formatting.default:Default", + "flake8.report", ), ), finder.Plugin( "flake8", "9001", importlib.metadata.EntryPoint( - "pylint", "flake8.formatting.default:Pylint", "flake8.report" + "pylint", "flake8.formatting.default:Pylint", "flake8.report", ), ), finder.Plugin( "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "Q", "flake8_foo:Plugin", "flake8.extension" + "Q", "flake8_foo:Plugin", "flake8.extension", ), ), finder.Plugin( "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "foo", "flake8_foo:Formatter", "flake8.report" + "foo", "flake8_foo:Formatter", "flake8.report", ), ), finder.Plugin( @@ -518,7 +524,7 @@ def test_find_plugins( "local", "local", importlib.metadata.EntryPoint( - "Y", "mod2:attr", "flake8.extension" + "Y", "mod2:attr", "flake8.extension", ), ), finder.Plugin( @@ -723,7 +729,7 @@ def test_import_plugins_extends_sys_path(): def test_classify_plugins(): report_plugin = _loaded( - plugin=_plugin(ep=_ep(name="R", group="flake8.report")) + plugin=_plugin(ep=_ep(name="R", group="flake8.report")), ) tree_plugin = _loaded(parameters={"tree": True}) logical_line_plugin = _loaded(parameters={"logical_line": True}) diff --git a/tests/unit/plugins/reporter_test.py b/tests/unit/plugins/reporter_test.py index 842465a..48b2873 100644 --- a/tests/unit/plugins/reporter_test.py +++ b/tests/unit/plugins/reporter_test.py @@ -25,7 +25,7 @@ def reporters(): "flake8", "123", importlib.metadata.EntryPoint( - name, f"{cls.__module__}:{cls.__name__}", "flake8.report" + name, f"{cls.__module__}:{cls.__name__}", "flake8.report", ), ), cls, @@ -72,5 +72,5 @@ def test_make_formatter_format_string(reporters, caplog): "flake8.plugins.reporter", 30, "'hi %(code)s' is an unknown formatter. Falling back to default.", - ) + ), ] diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 04147ec..3c93085 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -36,7 +36,7 @@ def application(): ], ) def test_application_exit_code( - result_count, catastrophic, exit_zero, value, application + result_count, catastrophic, exit_zero, value, application, ): """Verify Application.exit_code returns the correct value.""" application.result_count = result_count diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py index 5b57335..0d81c81 100644 --- a/tests/unit/test_base_formatter.py +++ b/tests/unit/test_base_formatter.py @@ -50,7 +50,7 @@ def test_format_needs_to_be_implemented(): formatter = base.BaseFormatter(options()) with pytest.raises(NotImplementedError): formatter.format( - Violation("A000", "file.py", 1, 1, "error text", None) + Violation("A000", "file.py", 1, 1, "error text", None), ) @@ -59,7 +59,7 @@ def test_show_source_returns_nothing_when_not_showing_source(): formatter = base.BaseFormatter(options(show_source=False)) assert ( formatter.show_source( - Violation("A000", "file.py", 1, 1, "error text", "line") + Violation("A000", "file.py", 1, 1, "error text", "line"), ) == "" ) @@ -70,7 +70,7 @@ def test_show_source_returns_nothing_when_there_is_source(): formatter = base.BaseFormatter(options(show_source=True)) assert ( formatter.show_source( - Violation("A000", "file.py", 1, 1, "error text", None) + Violation("A000", "file.py", 1, 1, "error text", None), ) == "" ) diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index 4ba604f..298b598 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -14,7 +14,7 @@ def test_debug_information(): pkg, version, importlib.metadata.EntryPoint( - ep_name, "dne:dne", "flake8.extension" + ep_name, "dne:dne", "flake8.extension", ), ), None, diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index d543d5e..cd8f80d 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -35,7 +35,7 @@ def create_options(**kwargs): def test_was_ignored_ignores_errors(ignore_list, extend_ignore, error_code): """Verify we detect users explicitly ignoring an error.""" decider = style_guide.DecisionEngine( - create_options(ignore=ignore_list, extend_ignore=extend_ignore) + create_options(ignore=ignore_list, extend_ignore=extend_ignore), ) assert decider.was_ignored(error_code) is style_guide.Ignored.Explicitly @@ -53,11 +53,11 @@ def test_was_ignored_ignores_errors(ignore_list, extend_ignore, error_code): ], ) def test_was_ignored_implicitly_selects_errors( - ignore_list, extend_ignore, error_code + ignore_list, extend_ignore, error_code, ): """Verify we detect users does not explicitly ignore an error.""" decider = style_guide.DecisionEngine( - create_options(ignore=ignore_list, extend_ignore=extend_ignore) + create_options(ignore=ignore_list, extend_ignore=extend_ignore), ) assert decider.was_ignored(error_code) is style_guide.Selected.Implicitly @@ -179,7 +179,7 @@ def test_was_selected_excludes_errors(select_list, error_code): ], ) def test_decision_for( - select_list, ignore_list, extend_ignore, error_code, expected + select_list, ignore_list, extend_ignore, error_code, expected, ): """Verify we decide when to report an error.""" decider = style_guide.DecisionEngine( @@ -187,7 +187,7 @@ def test_decision_for( select=select_list, ignore=ignore_list, extend_ignore=extend_ignore, - ) + ), ) assert decider.decision_for(error_code) is expected diff --git a/tests/unit/test_discover_files.py b/tests/unit/test_discover_files.py index ca945c2..ea55ccc 100644 --- a/tests/unit/test_discover_files.py +++ b/tests/unit/test_discover_files.py @@ -47,7 +47,7 @@ def test_filenames_from_a_directory_with_a_predicate(): _filenames_from( arg=_normpath("a/b/"), predicate=lambda path: path.endswith(_normpath("b/c.py")), - ) + ), ) # should not include c.py expected = _normpaths(("a/b/d.py", "a/b/e/f.py")) @@ -61,7 +61,7 @@ def test_filenames_from_a_directory_with_a_predicate_from_the_current_dir(): _filenames_from( arg=_normpath("./a/b"), predicate=lambda path: path == "c.py", - ) + ), ) # none should have matched the predicate so all returned expected = _normpaths(("./a/b/c.py", "./a/b/d.py", "./a/b/e/f.py")) @@ -132,7 +132,7 @@ def _expand_paths( stdin_display_name=stdin_display_name, filename_patterns=filename_patterns, exclude=exclude, - ) + ), ) diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index a90c628..22c5bcf 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -28,7 +28,7 @@ def _lines_from_file(tmpdir, contents, options): def test_read_lines_universal_newlines(tmpdir, default_options): r"""Verify that line endings are translated to \n.""" lines = _lines_from_file( - tmpdir, b"# coding: utf-8\r\nx = 1\r\n", default_options + tmpdir, b"# coding: utf-8\r\nx = 1\r\n", default_options, ) assert lines == ["# coding: utf-8\n", "x = 1\n"] @@ -36,7 +36,7 @@ def test_read_lines_universal_newlines(tmpdir, default_options): def test_read_lines_incorrect_utf_16(tmpdir, default_options): """Verify that an incorrectly encoded file is read as latin-1.""" lines = _lines_from_file( - tmpdir, b"# coding: utf16\nx = 1\n", default_options + tmpdir, b"# coding: utf16\nx = 1\n", default_options, ) assert lines == ["# coding: utf16\n", "x = 1\n"] @@ -44,7 +44,7 @@ def test_read_lines_incorrect_utf_16(tmpdir, default_options): def test_read_lines_unknown_encoding(tmpdir, default_options): """Verify that an unknown encoding is still read as latin-1.""" lines = _lines_from_file( - tmpdir, b"# coding: fake-encoding\nx = 1\n", default_options + tmpdir, b"# coding: fake-encoding\nx = 1\n", default_options, ) assert lines == ["# coding: fake-encoding\n", "x = 1\n"] @@ -289,7 +289,7 @@ def test_processor_split_line(default_options): def test_build_ast(default_options): """Verify the logic for how we build an AST for plugins.""" file_processor = processor.FileProcessor( - "-", default_options, lines=["a = 1\n"] + "-", default_options, lines=["a = 1\n"], ) module = file_processor.build_ast() @@ -299,7 +299,7 @@ def test_build_ast(default_options): def test_next_logical_line_updates_the_previous_logical_line(default_options): """Verify that we update our tracking of the previous logical line.""" file_processor = processor.FileProcessor( - "-", default_options, lines=["a = 1\n"] + "-", default_options, lines=["a = 1\n"], ) file_processor.indent_level = 1 @@ -315,7 +315,7 @@ def test_next_logical_line_updates_the_previous_logical_line(default_options): def test_visited_new_blank_line(default_options): """Verify we update the number of blank lines seen.""" file_processor = processor.FileProcessor( - "-", default_options, lines=["a = 1\n"] + "-", default_options, lines=["a = 1\n"], ) assert file_processor.blank_lines == 0 diff --git a/tests/unit/test_main_options.py b/tests/unit/test_main_options.py index 7c1feba..0b1fb69 100644 --- a/tests/unit/test_main_options.py +++ b/tests/unit/test_main_options.py @@ -6,7 +6,7 @@ from flake8.main import options def test_stage1_arg_parser(): stage1_parser = options.stage1_arg_parser() opts, args = stage1_parser.parse_known_args( - ["--foo", "--verbose", "src", "setup.py", "--statistics", "--version"] + ["--foo", "--verbose", "src", "setup.py", "--statistics", "--version"], ) assert opts.verbose diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index 92266f3..9904a2e 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -122,7 +122,7 @@ def test_parse_args_handles_comma_separated_defaults(optmanager): assert optmanager.config_options_dict == {} optmanager.add_option( - "--exclude", default="E123,W234", comma_separated_list=True + "--exclude", default="E123,W234", comma_separated_list=True, ) options = optmanager.parse_args([]) @@ -135,7 +135,7 @@ def test_parse_args_handles_comma_separated_lists(optmanager): assert optmanager.config_options_dict == {} optmanager.add_option( - "--exclude", default="E123,W234", comma_separated_list=True + "--exclude", default="E123,W234", comma_separated_list=True, ) options = optmanager.parse_args(["--exclude", "E201,W111,F280"]) @@ -148,11 +148,11 @@ def test_parse_args_normalize_paths(optmanager): assert optmanager.config_options_dict == {} optmanager.add_option( - "--extra-config", normalize_paths=True, comma_separated_list=True + "--extra-config", normalize_paths=True, comma_separated_list=True, ) options = optmanager.parse_args( - ["--extra-config", "../config.ini,tox.ini,flake8/some-other.cfg"] + ["--extra-config", "../config.ini,tox.ini,flake8/some-other.cfg"], ) assert options.extra_config == [ os.path.abspath("../config.ini"), diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index 7de58f0..d73f471 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -169,7 +169,7 @@ def test_load_extra_config_utf8(tmpdir): @pytest.fixture def opt_manager(): ret = OptionManager( - version="123", plugin_versions="", parents=[], formatter_names=[] + version="123", plugin_versions="", parents=[], formatter_names=[], ) register_default_options(ret) return ret @@ -213,7 +213,7 @@ def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog): "flake8.options.config", 10, 'Option "wat" is not registered. Ignoring.', - ) + ), ] diff --git a/tests/unit/test_style_guide.py b/tests/unit/test_style_guide.py index 94fcb26..c66cfd2 100644 --- a/tests/unit/test_style_guide.py +++ b/tests/unit/test_style_guide.py @@ -36,7 +36,7 @@ def test_handle_error_does_not_raise_type_errors(): ) assert 1 == guide.handle_error( - "T111", "file.py", 1, 1, "error found", "a = 1" + "T111", "file.py", 1, 1, "error found", "a = 1", ) @@ -110,7 +110,7 @@ def test_style_guide_manager_pre_file_ignores_parsing(): ], ) def test_style_guide_manager_pre_file_ignores( - ignores, violation, filename, handle_error_return + ignores, violation, filename, handle_error_return, ): """Verify how the StyleGuideManager creates a default style guide.""" formatter = mock.create_autospec(base.BaseFormatter, instance=True) From 0f1af5010897439534e95dd4ec38d2738a136bc5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:56:25 +0000 Subject: [PATCH 248/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.17.0 → v1.17.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.0...v1.17.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 837e6e5..d3fd0cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.0 + rev: v1.17.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 3a2eff0868553e6cec0930bb996ed7a05acd8d52 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:22:04 +0000 Subject: [PATCH 249/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3fd0cc..30581db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-yaml - id: debug-statements From 4b13c2cc19ef9436a1048a2dfcf0ee934d17dbf4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 21:47:50 +0000 Subject: [PATCH 250/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.17.1 → v1.18.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.1...v1.18.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30581db..14ac934 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From e9f1cf3f484498c98c41f16c61c676b633b6c49d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 22:17:05 +0000 Subject: [PATCH 251/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.18.1 → v1.18.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.1...v1.18.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14ac934..20e006f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.1 + rev: v1.18.2 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 567cafc15a0b20ad814579c17b306c058029334c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Oct 2025 10:01:02 -0400 Subject: [PATCH 252/257] py310+ --- .github/workflows/main.yml | 14 ++++++------ .pre-commit-config.yaml | 12 +++++----- bin/gen-pycodestyle-plugin | 2 +- setup.cfg | 2 +- src/flake8/checker.py | 37 ------------------------------- src/flake8/discover_files.py | 2 +- src/flake8/options/manager.py | 2 +- tests/integration/test_checker.py | 16 ++++--------- tests/integration/test_main.py | 12 ++++------ 9 files changed, 25 insertions(+), 74 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08f54ea..486b0cb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,10 +13,7 @@ jobs: include: # linux - os: ubuntu-latest - python: pypy-3.9 - toxenv: py - - os: ubuntu-latest - python: 3.9 + python: pypy-3.11 toxenv: py - os: ubuntu-latest python: '3.10' @@ -30,9 +27,12 @@ jobs: - os: ubuntu-latest python: '3.13' toxenv: py + - os: ubuntu-latest + python: '3.14' + toxenv: py # windows - os: windows-latest - python: 3.9 + python: '3.10' toxenv: py # misc - os: ubuntu-latest @@ -46,8 +46,8 @@ jobs: toxenv: dogfood runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: python -mpip install --upgrade setuptools pip tox virtualenv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20e006f..2fac6b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/add-trailing-comma - rev: v3.2.0 + rev: v4.0.0 hooks: - id: add-trailing-comma - repo: https://github.com/pre-commit/pre-commit-hooks @@ -12,23 +12,23 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.8.0 + rev: v3.1.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.15.0 + rev: v3.16.0 hooks: - id: reorder-python-imports args: [ --application-directories, '.:src', - --py39-plus, + --py310-plus, --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/hhatto/autopep8 rev: v2.3.2 hooks: diff --git a/bin/gen-pycodestyle-plugin b/bin/gen-pycodestyle-plugin index c93fbfe..7fc504a 100755 --- a/bin/gen-pycodestyle-plugin +++ b/bin/gen-pycodestyle-plugin @@ -3,9 +3,9 @@ from __future__ import annotations import inspect import os.path +from collections.abc import Callable from collections.abc import Generator from typing import Any -from typing import Callable from typing import NamedTuple import pycodestyle diff --git a/setup.cfg b/setup.cfg index dc967d3..c0b8137 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.14.0,<2.15.0 pyflakes>=3.4.0,<3.5.0 -python_requires = >=3.9 +python_requires = >=3.10 package_dir = =src diff --git a/src/flake8/checker.py b/src/flake8/checker.py index d957915..c6a24eb 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -372,43 +372,6 @@ class FileChecker: token = () row, column = (1, 0) - if ( - column > 0 - and token - and isinstance(exception, SyntaxError) - and len(token) == 4 # Python 3.9 or earlier - ): - # NOTE(sigmavirus24): SyntaxErrors report 1-indexed column - # numbers. We need to decrement the column number by 1 at - # least. - column_offset = 1 - row_offset = 0 - # See also: https://github.com/pycqa/flake8/issues/169, - # https://github.com/PyCQA/flake8/issues/1372 - # On Python 3.9 and earlier, token will be a 4-item tuple with the - # last item being the string. Starting with 3.10, they added to - # the tuple so now instead of it ending with the code that failed - # to parse, it ends with the end of the section of code that - # failed to parse. Luckily the absolute position in the tuple is - # stable across versions so we can use that here - physical_line = token[3] - - # NOTE(sigmavirus24): Not all "tokens" have a string as the last - # argument. In this event, let's skip trying to find the correct - # column and row values. - if physical_line is not None: - # NOTE(sigmavirus24): SyntaxErrors also don't exactly have a - # "physical" line so much as what was accumulated by the point - # tokenizing failed. - # See also: https://github.com/pycqa/flake8/issues/169 - lines = physical_line.rstrip("\n").split("\n") - row_offset = len(lines) - 1 - logical_line = lines[0] - logical_line_length = len(logical_line) - if column > logical_line_length: - column = logical_line_length - row -= row_offset - column -= column_offset return row, column def run_ast_checks(self) -> None: diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py index da28ba5..40b6e5c 100644 --- a/src/flake8/discover_files.py +++ b/src/flake8/discover_files.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging import os.path +from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence -from typing import Callable from flake8 import utils diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index addd3ec..ae40794 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -5,9 +5,9 @@ import argparse import enum import functools import logging +from collections.abc import Callable from collections.abc import Sequence from typing import Any -from typing import Callable from flake8 import utils from flake8.plugins.finder import Plugins diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index 60d0132..f7f07af 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -2,7 +2,6 @@ from __future__ import annotations import importlib.metadata -import sys from unittest import mock import pytest @@ -322,17 +321,10 @@ def test_handling_syntaxerrors_across_pythons(): We need to handle that correctly to avoid crashing. https://github.com/PyCQA/flake8/issues/1372 """ - if sys.version_info < (3, 10): # pragma: no cover (<3.10) - # Python 3.9 or older - err = SyntaxError( - "invalid syntax", ("", 2, 5, "bad python:\n"), - ) - expected = (2, 4) - else: # pragma: no cover (3.10+) - err = SyntaxError( - "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11), - ) - expected = (2, 1) + err = SyntaxError( + "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11), + ) + expected = (2, 1) file_checker = checker.FileChecker( filename="-", plugins=finder.Checkers([], [], []), diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 5091573..0ca5b63 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -168,10 +168,8 @@ def test_tokenization_error_but_not_syntax_error(tmpdir, capsys): tmpdir.join("t.py").write("b'foo' \\\n") assert cli.main(["t.py"]) == 1 - if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) - expected = "t.py:2:1: E999 SyntaxError: end of file (EOF) in multi-line statement\n" # noqa: E501 - elif sys.version_info < (3, 10): # pragma: no cover (cp38+) - expected = "t.py:1:8: E999 SyntaxError: unexpected EOF while parsing\n" + if sys.implementation.name == "pypy": # pragma: no cover (pypy) + expected = "t.py:1:9: E999 SyntaxError: unexpected end of file (EOF) in multi-line statement\n" # noqa: E501 else: # pragma: no cover (cp310+) expected = "t.py:1:10: E999 SyntaxError: unexpected EOF while parsing\n" # noqa: E501 @@ -186,10 +184,8 @@ def test_tokenization_error_is_a_syntax_error(tmpdir, capsys): tmpdir.join("t.py").write("if True:\n pass\n pass\n") assert cli.main(["t.py"]) == 1 - if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) - expected = "t.py:3:2: E999 IndentationError: unindent does not match any outer indentation level\n" # noqa: E501 - elif sys.version_info < (3, 10): # pragma: no cover ( Date: Mon, 10 Nov 2025 22:38:48 +0000 Subject: [PATCH 253/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.21.0 → v3.21.1](https://github.com/asottile/pyupgrade/compare/v3.21.0...v3.21.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fac6b3..c35ff64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.21.0 + rev: v3.21.1 hooks: - id: pyupgrade args: [--py310-plus] From 01af84d980b9d467e89cbb27e619eb1346c1c673 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:27:23 +0000 Subject: [PATCH 254/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.21.1 → v3.21.2](https://github.com/asottile/pyupgrade/compare/v3.21.1...v3.21.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c35ff64..02808bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.21.1 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py310-plus] From 72c267d2e5eef80b59ff05dbe31a000b22ed4615 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:40:09 +0000 Subject: [PATCH 255/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.18.2 → v1.19.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.2...v1.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02808bf..8108d1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 45c1af5e2426ff5a3638b6c771f3d27ebed92fbd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:21:22 +0000 Subject: [PATCH 256/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.19.0 → v1.19.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.19.0...v1.19.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8108d1e..7d6a6ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.19.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) From 941f908d6c0fa0aff9993ed1c1bb4e0aa11e9847 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:52:21 +0000 Subject: [PATCH 257/257] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v3.1.0 → v3.2.0](https://github.com/asottile/setup-cfg-fmt/compare/v3.1.0...v3.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d6a6ca..f75e5ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/setup-cfg-fmt - rev: v3.1.0 + rev: v3.2.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports