Skip to content

Commit e69f6f4

Browse files
author
Aleksey Petryankin
committed
Review-changes 4: fix score when use-local-configs=y
1 parent 655ef69 commit e69f6f4

File tree

2 files changed

+86
-18
lines changed

2 files changed

+86
-18
lines changed

pylint/lint/pylinter.py

+27-17
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
ModuleDescriptionDict,
7070
Options,
7171
)
72-
from pylint.utils import ASTWalker, FileState, LinterStats, utils
72+
from pylint.utils import ASTWalker, FileState, LinterStats, merge_stats, utils
7373

7474
MANAGER = astroid.MANAGER
7575

@@ -320,6 +320,7 @@ def __init__(
320320

321321
# Attributes related to stats
322322
self.stats = LinterStats()
323+
self.all_stats: list[LinterStats] = []
323324

324325
# Attributes related to (command-line) options and their parsing
325326
self.options: Options = options + _make_linter_options(self)
@@ -951,12 +952,17 @@ def _expand_files(
951952
def set_current_module(self, modname: str, filepath: str | None = None) -> None:
952953
"""Set the name of the currently analyzed module and
953954
init statistics for it.
955+
956+
Save current stats before init to make sure no counters for
957+
error, statement, etc are missed.
954958
"""
955959
if not modname and filepath is None:
956960
return
957961
self.reporter.on_set_current_module(modname or "", filepath)
958962
self.current_name = modname
959963
self.current_file = filepath or modname
964+
self.all_stats.append(self.stats)
965+
self.stats = LinterStats()
960966
self.stats.init_single_module(modname or "")
961967

962968
# If there is an actual filepath we might need to update the config attribute
@@ -1013,7 +1019,7 @@ def _astroid_module_checker(
10131019
rawcheckers=rawcheckers,
10141020
)
10151021

1016-
# notify global end
1022+
# notify end of module if jobs>1 or use-local-configs=y, global end otherwise
10171023
self.stats.statement = walker.nbstatements
10181024
for checker in reversed(_checkers):
10191025
checker.close()
@@ -1147,14 +1153,18 @@ def generate_reports(self, verbose: bool = False) -> int | None:
11471153
11481154
if persistent run, pickle results for later comparison
11491155
"""
1156+
self.config = self._base_config
11501157
# Display whatever messages are left on the reporter.
11511158
self.reporter.display_messages(report_nodes.Section())
1159+
# current self.stats is needed in merge - it contains stats from last module
1160+
# separate variable to avoid modifying any stats during reporting
1161+
self.finished_run_stats = merge_stats([self.stats, *self.all_stats])
11521162
if not self.file_state._is_base_filestate:
11531163
# load previous results if any
11541164
previous_stats = load_results(self.file_state.base_name)
1155-
self.reporter.on_close(self.stats, previous_stats)
1165+
self.reporter.on_close(self.finished_run_stats, previous_stats)
11561166
if self.config.reports:
1157-
sect = self.make_reports(self.stats, previous_stats)
1167+
sect = self.make_reports(self.finished_run_stats, previous_stats)
11581168
else:
11591169
sect = report_nodes.Section()
11601170

@@ -1163,9 +1173,9 @@ def generate_reports(self, verbose: bool = False) -> int | None:
11631173
score_value = self._report_evaluation(verbose)
11641174
# save results if persistent run
11651175
if self.config.persistent:
1166-
save_results(self.stats, self.file_state.base_name)
1176+
save_results(self.finished_run_stats, self.file_state.base_name)
11671177
else:
1168-
self.reporter.on_close(self.stats, LinterStats())
1178+
self.reporter.on_close(self.finished_run_stats, LinterStats())
11691179
score_value = None
11701180
return score_value
11711181

@@ -1175,35 +1185,35 @@ def _report_evaluation(self, verbose: bool = False) -> int | None:
11751185
# syntax error preventing pylint from further processing)
11761186
note = None
11771187
previous_stats = load_results(self.file_state.base_name)
1178-
if self.stats.statement == 0:
1188+
if self.finished_run_stats.statement == 0:
11791189
return note
11801190

11811191
# get a global note for the code
11821192
evaluation = self.config.evaluation
11831193
try:
11841194
stats_dict = {
1185-
"fatal": self.stats.fatal,
1186-
"error": self.stats.error,
1187-
"warning": self.stats.warning,
1188-
"refactor": self.stats.refactor,
1189-
"convention": self.stats.convention,
1190-
"statement": self.stats.statement,
1191-
"info": self.stats.info,
1195+
"fatal": self.finished_run_stats.fatal,
1196+
"error": self.finished_run_stats.error,
1197+
"warning": self.finished_run_stats.warning,
1198+
"refactor": self.finished_run_stats.refactor,
1199+
"convention": self.finished_run_stats.convention,
1200+
"statement": self.finished_run_stats.statement,
1201+
"info": self.finished_run_stats.info,
11921202
}
11931203
note = eval(evaluation, {}, stats_dict) # pylint: disable=eval-used
11941204
except Exception as ex: # pylint: disable=broad-except
11951205
msg = f"An exception occurred while rating: {ex}"
11961206
else:
1197-
self.stats.global_note = note
1207+
self.finished_run_stats.global_note = note
11981208
msg = f"Your code has been rated at {note:.2f}/10"
11991209
if previous_stats:
12001210
pnote = previous_stats.global_note
12011211
if pnote is not None:
12021212
msg += f" (previous run: {pnote:.2f}/10, {note - pnote:+.2f})"
12031213

12041214
if verbose:
1205-
checked_files_count = self.stats.node_count["module"]
1206-
unchecked_files_count = self.stats.undocumented["module"]
1215+
checked_files_count = self.finished_run_stats.node_count["module"]
1216+
unchecked_files_count = self.finished_run_stats.undocumented["module"]
12071217
msg += f"\nChecked {checked_files_count} files, skipped {unchecked_files_count} files"
12081218

12091219
if self.config.score:

tests/config/test_per_directory_config.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66

77
import os
88
import os.path
9+
from argparse import Namespace
910
from io import StringIO
1011
from pathlib import Path
12+
from typing import Any
13+
from unittest.mock import patch
1114

1215
import pytest
1316
from pytest import CaptureFixture
1417

1518
from pylint.lint import Run as LintRun
19+
from pylint.lint.pylinter import PyLinter
1620
from pylint.testutils._run import _Run as Run
1721
from pylint.testutils.utils import _patch_streams, _test_cwd
1822

@@ -52,7 +56,7 @@ def _create_subconfig_test_fs(tmp_path: Path) -> tuple[Path, ...]:
5256
level1_init.touch()
5357
level1_init2.touch()
5458
level2_init.touch()
55-
test_file_text = "#LEVEL1\n#LEVEL2\n#ALL_LEVELS\n#TODO\n"
59+
test_file_text = "#LEVEL1\n#LEVEL2\n#ALL_LEVELS\n#TODO\nassert (1, None)\ns = 'statement without warnings'\n"
5660
test_file1.write_text(test_file_text)
5761
test_file2.write_text(test_file_text)
5862
test_file3.write_text(test_file_text)
@@ -250,3 +254,57 @@ def test_local_config_verbose(
250254
LintRun(["--verbose", "--use-local-configs=y", str(tmp_files[1])], exit=False)
251255
output = capsys.readouterr()
252256
assert f"Using config from {level1_dir / 'sub'}" in output.err
257+
258+
259+
def ns_diff(ns1: Namespace, ns2: Namespace) -> str:
260+
msg = "Namespaces not equal\n"
261+
for k, v in ns1.__dict__.items():
262+
if v != ns2.__dict__[k]:
263+
msg += f"{v} != {ns2.__dict__[k]}\n"
264+
return msg
265+
266+
267+
generate_reports_orig = PyLinter.generate_reports
268+
269+
270+
def generate_reports_spy(self: PyLinter, *args: Any, **kwargs: Any) -> int:
271+
score = generate_reports_orig(self, *args, **kwargs)
272+
# check that generate_reports() worked with base config, not config from most recent module
273+
assert self.config == self._base_config, ns_diff(self.config, self._base_config)
274+
# level1_dir.a, level1_dir.z, level1_dir.sub.b from _create_subconfig_test_fs
275+
# each has 2 statements, one of which is warning => score should be 5
276+
assert score is not None
277+
assert 0 < score < 10
278+
return score
279+
280+
281+
@pytest.mark.parametrize(
282+
"local_config_args",
283+
[["--use-local-configs=y"], ["--use-local-configs=y", "--jobs=2"]],
284+
)
285+
def test_subconfigs_score(
286+
_create_subconfig_test_fs: tuple[Path, ...],
287+
local_config_args: list[str],
288+
) -> None:
289+
"""Check that statements from all checked modules are accounted in score:
290+
given stats from many modules such that
291+
total # of messages > statements in last module,
292+
check that score is >0 and <10.
293+
"""
294+
level1_dir, *_ = _create_subconfig_test_fs
295+
out = StringIO()
296+
with (
297+
patch(
298+
"pylint.lint.run.Run.LinterClass.generate_reports",
299+
side_effect=generate_reports_spy,
300+
autospec=True,
301+
) as reports_patch,
302+
_patch_streams(out),
303+
):
304+
linter = LintRun([*local_config_args, str(level1_dir)], exit=False).linter
305+
reports_patch.assert_called_once()
306+
307+
# level1_dir.a, level1_dir.z, level1_dir.sub.b from _create_subconfig_test_fs
308+
# each has 2 statements, one of which is warning, so 3 warnings total
309+
assert linter.finished_run_stats.statement == 6
310+
assert linter.finished_run_stats.warning == 3

0 commit comments

Comments
 (0)