Skip to content

Commit 4e48d46

Browse files
Load custom plugins when linting in parallel (#8683)
1 parent aed3c08 commit 4e48d46

File tree

5 files changed

+36
-8
lines changed

5 files changed

+36
-8
lines changed

doc/user_guide/usage/run.rst

+4-4
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ This will spawn 4 parallel Pylint sub-process, where each provided module will
160160
be checked in parallel. Discovered problems by checkers are not displayed
161161
immediately. They are shown just after checking a module is complete.
162162

163-
There are some limitations in running checks in parallel in the current
164-
implementation. It is not possible to use custom plugins
165-
(i.e. ``--load-plugins`` option), nor it is not possible to use
166-
initialization hooks (i.e. the ``--init-hook`` option).
163+
There is one known limitation with running checks in parallel as currently
164+
implemented. Since the division of files into worker processes is indeterminate,
165+
checkers that depend on comparing multiple files (e.g. ``cyclic-import``
166+
and ``duplicate-code``) can produce indeterminate results.
167167

168168
Exit codes
169169
----------

doc/whatsnew/fragments/4874.bugfix

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``--jobs`` can now be used with ``--load-plugins``.
2+
3+
This had regressed in astroid 2.5.0.
4+
5+
Closes #4874

pylint/lint/parallel.py

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ def _worker_initialize(
5252
_worker_linter.set_reporter(reporters.CollectingReporter())
5353
_worker_linter.open()
5454

55+
# Re-register dynamic plugins, since the pool does not have access to the
56+
# astroid module that existed when the linter was pickled.
57+
_worker_linter.load_plugin_modules(_worker_linter._dynamic_plugins, force=True)
58+
_worker_linter.load_plugin_configuration()
59+
5560
if extra_packages_paths:
5661
_augment_sys_path(extra_packages_paths)
5762

pylint/lint/pylinter.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import tokenize
1414
import traceback
1515
from collections import defaultdict
16-
from collections.abc import Callable, Iterator, Sequence
16+
from collections.abc import Callable, Iterable, Iterator, Sequence
1717
from io import TextIOWrapper
1818
from pathlib import Path
1919
from re import Pattern
@@ -363,15 +363,18 @@ def load_default_plugins(self) -> None:
363363
checkers.initialize(self)
364364
reporters.initialize(self)
365365

366-
def load_plugin_modules(self, modnames: list[str]) -> None:
366+
def load_plugin_modules(self, modnames: Iterable[str], force: bool = False) -> None:
367367
"""Check a list of pylint plugins modules, load and register them.
368368
369369
If a module cannot be loaded, never try to load it again and instead
370370
store the error message for later use in ``load_plugin_configuration``
371371
below.
372+
373+
If `force` is True (useful when multiprocessing), then the plugin is
374+
reloaded regardless if an entry exists in self._dynamic_plugins.
372375
"""
373376
for modname in modnames:
374-
if modname in self._dynamic_plugins:
377+
if modname in self._dynamic_plugins and not force:
375378
continue
376379
try:
377380
module = astroid.modutils.load_module_from_name(modname)

tests/test_check_parallel.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
from concurrent.futures import ProcessPoolExecutor
1515
from concurrent.futures.process import BrokenProcessPool
1616
from pickle import PickleError
17+
from typing import TYPE_CHECKING
18+
from unittest.mock import patch
1719

1820
import dill
1921
import pytest
20-
from astroid import nodes
2122

2223
import pylint.interfaces
2324
import pylint.lint.parallel
@@ -30,6 +31,9 @@
3031
from pylint.typing import FileItem
3132
from pylint.utils import LinterStats, ModuleStats
3233

34+
if TYPE_CHECKING:
35+
from astroid import nodes
36+
3337

3438
def _gen_file_data(idx: int = 0) -> FileItem:
3539
"""Generates a file to use as a stream."""
@@ -182,6 +186,17 @@ def test_worker_initialize_with_package_paths(self) -> None:
182186
)
183187
assert "fake-path" in sys.path
184188

189+
def test_worker_initialize_reregisters_custom_plugins(self) -> None:
190+
linter = PyLinter(reporter=Reporter())
191+
linter.load_plugin_modules(["pylint.extensions.private_import"])
192+
193+
pickled = dill.dumps(linter)
194+
with patch(
195+
"pylint.extensions.private_import.register", side_effect=AssertionError
196+
):
197+
with pytest.raises(AssertionError):
198+
worker_initialize(linter=pickled)
199+
185200
@pytest.mark.needs_two_cores
186201
def test_worker_initialize_pickling(self) -> None:
187202
"""Test that we can pickle objects that standard pickling in multiprocessing can't.

0 commit comments

Comments
 (0)