Skip to content

Commit bbdc1da

Browse files
KotlinIslandKotlinIsland
KotlinIsland
authored andcommitted
baseline allow
1 parent bae241b commit bbdc1da

File tree

7 files changed

+170
-35
lines changed

7 files changed

+170
-35
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Enable stub mode within `TYPE_CHECKING` branches (#702)
99
- Infer from overloads - add default value in impl (#697)
1010
- Warn for missing returns with explicit `Any` return types (#715)
11+
- `--baseline-allow` and `--baseline-ban` for baseline management (#710)
1112
### Fixes
1213
- positional arguments on overloads break super (#697)
1314
- positional arguments on overloads duplicate unions (#697)

mypy/build.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
is_sub_path,
6767
is_typeshed_file,
6868
module_prefix,
69+
plural_s,
6970
read_py_file,
7071
time_ref,
7172
time_spent_us,
@@ -1073,14 +1074,14 @@ def save_baseline(manager: BuildManager):
10731074
# Indicate that writing was canceled
10741075
manager.options.write_baseline = False
10751076
return
1076-
new_baseline = manager.errors.prepare_baseline_errors()
1077+
new_baseline, rejected = manager.errors.prepare_baseline_errors()
10771078
file = Path(manager.options.baseline_file)
10781079
if not new_baseline:
10791080
if file.exists():
10801081
file.unlink()
1081-
print("No errors, baseline file removed")
1082+
print("No baselinable errors, baseline file removed")
10821083
elif manager.options.write_baseline:
1083-
print("No errors, no baseline to write")
1084+
print("No baselinable errors, no baseline to write")
10841085
# Indicate that writing was canceled
10851086
manager.options.write_baseline = False
10861087
return
@@ -1096,7 +1097,15 @@ def save_baseline(manager: BuildManager):
10961097
with file.open("w") as f:
10971098
json.dump(data, f, indent=2, sort_keys=True)
10981099
if not manager.options.write_baseline and manager.options.auto_baseline:
1099-
manager.stdout.write(f"Baseline successfully updated at {file}\n")
1100+
removed = len(
1101+
[
1102+
error
1103+
for file in manager.errors.original_baseline.values()
1104+
for error in file
1105+
if error["code"].startswith("error:")
1106+
]
1107+
) - len(manager.errors.baseline_stats["total"])
1108+
manager.stdout.write(f"{removed} error{plural_s(removed)} removed from baseline {file}\n")
11001109

11011110

11021111
def load_baseline(options: Options, errors: Errors, stdout: TextIO) -> None:

mypy/errors.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -1362,7 +1362,9 @@ def initialize_baseline(
13621362
self.baseline = baseline_errors
13631363
self.baseline_targets = targets
13641364

1365-
def prepare_baseline_errors(self) -> dict[str, list[StoredBaselineError]]:
1365+
def prepare_baseline_errors(
1366+
self,
1367+
) -> (dict[str, list[StoredBaselineError]], list[StoredBaselineError]):
13661368
"""Create a dict representing the error portion of an error baseline file"""
13671369

13681370
def remove_duplicates(errors: list[ErrorInfo]) -> list[ErrorInfo]:
@@ -1386,10 +1388,29 @@ def remove_duplicates(errors: list[ErrorInfo]) -> list[ErrorInfo]:
13861388
i += 1
13871389
return unduplicated_result
13881390

1391+
allowed = self.options.baseline_allows
1392+
banned = self.options.baseline_allows
1393+
rejected = []
1394+
error_list = []
1395+
1396+
def gate_keep(error: ErrorInfo) -> bool:
1397+
"""should an error be accepted into the baseline"""
1398+
1399+
def yes_no(condition: bool) -> bool:
1400+
if error.severity == "error":
1401+
(error_list if condition else rejected).append(error)
1402+
return condition
1403+
1404+
if allowed:
1405+
return yes_no(error.code in allowed)
1406+
if banned:
1407+
return yes_no(error.code in banned)
1408+
return yes_no(True)
1409+
13891410
result = {
13901411
self.common_path(file): [
13911412
{
1392-
"code": error.code.code if error.code else None,
1413+
"code": f"{error.severity}:{error.code.code if error.code else None}",
13931414
"column": error.column,
13941415
"line": error.line,
13951416
"message": error.message,
@@ -1399,18 +1420,21 @@ def remove_duplicates(errors: list[ErrorInfo]) -> list[ErrorInfo]:
13991420
and cast(List[str], self.read_source(file))[error.line - 1].strip(),
14001421
}
14011422
for error in remove_duplicates(errors)
1423+
if gate_keep(error)
14021424
# don't store reveal errors
14031425
if error.code != codes.REVEAL
14041426
]
14051427
for file, errors in self.all_errors.items()
14061428
}
1429+
result = {file: errors for file, errors in result.items() if errors}
14071430
for file in result.values():
14081431
previous = 0
14091432
for error in file:
14101433
error["offset"] = cast(int, error["line"]) - previous
14111434
previous = cast(int, error["line"])
14121435
del error["line"]
1413-
return cast(Dict[str, List[StoredBaselineError]], result)
1436+
self.baseline_stats = {"total": error_list, "rejected": len(rejected)}
1437+
return cast(Dict[str, List[StoredBaselineError]], result), rejected
14141438

14151439
def filter_baseline(
14161440
self, errors: list[ErrorInfo], path: str, source_lines: list[str] | None

mypy/main.py

+64-24
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, get_search_dirs, mypy_path
2727
from mypy.options import COMPLETE_FEATURES, INCOMPLETE_FEATURES, BuildType, Options
2828
from mypy.split_namespace import SplitNamespace
29+
from mypy.util import plural_s
2930
from mypy.version import __based_version__, __version__
3031

3132
orig_stat: Final = os.stat
@@ -122,41 +123,58 @@ def main(
122123
if messages and n_notes < len(messages):
123124
code = 2 if blockers else 1
124125
if options.error_summary:
126+
if options.summary and res and n_errors:
127+
stdout.write("\n")
128+
stdout.write(formatter.style("error summary:\n", color="none", bold=True))
129+
all_errors = defaultdict(lambda: 0)
130+
for errors in res.manager.errors.error_info_map.values():
131+
for error in errors:
132+
if error.severity != "error" or not error.code:
133+
continue
134+
all_errors[error.code.code] += 1
135+
if all_errors:
136+
max_code_name_length = max(len(code_name) for code_name in all_errors)
137+
for code_name, count in all_errors.items():
138+
stdout.write(f" {code_name:<{max_code_name_length}} {count:>5}\n")
125139
if options.write_baseline and res:
126-
new_errors = n_errors
127140
n_files = len(res.manager.errors.all_errors)
128-
total = []
129-
# This is stupid, but it's just to remove the dupes from the unfiltered errors
130-
for errors in res.manager.errors.all_errors.values():
131-
temp = res.manager.errors.render_messages(errors)
132-
total.extend(res.manager.errors.remove_duplicates(temp))
133-
n_errors = len([error for error in total if error[5] == "error"])
134-
else:
135-
new_errors = -1
141+
stats = res.manager.errors.baseline_stats
142+
rejected = stats["rejected"]
143+
total = len(stats["total"])
144+
new_errors = n_errors - rejected
145+
previous = res.manager.errors.original_baseline
146+
stdout.write(formatter.style("baseline:\n", color="none", bold=True))
147+
stdout.write(f" {new_errors} new error{plural_s(new_errors)}\n")
148+
stdout.write(f" {total} error{plural_s(total)} in baseline\n")
149+
difference = (
150+
len(
151+
[
152+
error
153+
for file in previous.values()
154+
for error in file
155+
if error["code"].startswith("error:")
156+
]
157+
)
158+
- total
159+
)
160+
if difference > 0:
161+
stdout.write(
162+
f" {difference} error{plural_s(difference)} less than previous baseline\n"
163+
)
164+
if rejected:
165+
stdout.write(f" {rejected} error{plural_s(rejected)} rejected\n")
166+
stdout.write(f" baseline successfully written to {options.baseline_file}\n")
167+
stdout.flush()
136168
if n_errors:
137169
summary = formatter.format_error(
138-
n_errors,
139-
n_files,
140-
len(sources),
141-
new_errors=new_errors,
142-
blockers=blockers,
143-
use_color=options.color_output,
170+
n_errors, n_files, len(sources), blockers=blockers, use_color=options.color_output
144171
)
145172
stdout.write(summary + "\n")
146173
# Only notes should also output success
147174
elif not messages or n_notes == len(messages):
148175
stdout.write(formatter.format_success(len(sources), options.color_output) + "\n")
149176
stdout.flush()
150177

151-
if options.write_baseline:
152-
stdout.write(
153-
formatter.style(
154-
f"Baseline successfully written to {options.baseline_file}\n", "green", bold=True
155-
)
156-
)
157-
stdout.flush()
158-
code = 0
159-
160178
if options.install_types and not options.non_interactive:
161179
result = install_types(formatter, options, after_run=True, non_interactive=False)
162180
if result:
@@ -563,6 +581,20 @@ def add_invertible_flag(
563581
action="store",
564582
help=f"Use baseline info in the given file (defaults to '{defaults.BASELINE_FILE}')",
565583
)
584+
based_group.add_argument(
585+
"--baseline-allow",
586+
metavar="NAME",
587+
action="append",
588+
default=[],
589+
help="Allow error codes into the baseline",
590+
)
591+
based_group.add_argument(
592+
"--baseline-ban",
593+
metavar="NAME",
594+
action="append",
595+
default=[],
596+
help="Prevent error codes from being written to the baseline",
597+
)
566598
add_invertible_flag(
567599
"--no-auto-baseline",
568600
default=True,
@@ -619,6 +651,12 @@ def add_invertible_flag(
619651
"You probably want to set this on a module override",
620652
group=based_group,
621653
)
654+
add_invertible_flag(
655+
"--no-summary",
656+
default=False,
657+
help="don't show an error code summary at the end",
658+
group=based_group,
659+
)
622660
add_invertible_flag(
623661
"--ide", default=False, help="Best default for IDE integration.", group=based_group
624662
)
@@ -1460,6 +1498,8 @@ def set_ide_flags() -> None:
14601498
if invalid_codes:
14611499
parser.error(f"Invalid error code(s): {', '.join(sorted(invalid_codes))}")
14621500

1501+
options.baseline_bans |= {error_codes[code] for code in set(options.baseline_ban)}
1502+
options.baseline_allows |= {error_codes[code] for code in set(options.baseline_allow)}
14631503
options.disabled_error_codes |= {error_codes[code] for code in disabled_codes}
14641504
options.enabled_error_codes |= {error_codes[code] for code in enabled_codes}
14651505

mypy/options.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ def __init__(self) -> None:
165165
# Based options
166166
self.write_baseline = False
167167
self.baseline_file = defaults.BASELINE_FILE
168+
self.baseline_allow: list[str] = []
169+
self.baseline_allows: set[ErrorCode] = set()
170+
self.baseline_ban: list[str] = []
171+
self.baseline_bans: set[ErrorCode] = set()
172+
self.summary = True
168173
self.auto_baseline = True
169174
self.default_return = True
170175
self.infer_function_types = True
@@ -609,7 +614,12 @@ def select_options_affecting_cache(self) -> Mapping[str, object]:
609614
result: dict[str, object] = {}
610615
for opt in OPTIONS_AFFECTING_CACHE:
611616
val = getattr(self, opt)
612-
if opt in ("disabled_error_codes", "enabled_error_codes"):
617+
if opt in (
618+
"disabled_error_codes",
619+
"enabled_error_codes",
620+
"baseline_allows",
621+
"baseline_bans",
622+
):
613623
val = sorted([code.code for code in val])
614624
result[opt] = val
615625
return result

mypy/util.py

-3
Original file line numberDiff line numberDiff line change
@@ -824,12 +824,9 @@ def format_error(
824824
*,
825825
blockers: bool = False,
826826
use_color: bool = True,
827-
new_errors: int = -1,
828827
) -> str:
829828
"""Format a short summary in case of errors."""
830829
msg = f"Found {n_errors} error{plural_s(n_errors)} "
831-
if new_errors != -1:
832-
msg += f"({new_errors} new error{plural_s(new_errors)}) "
833830
msg += f"in {n_files} file{plural_s(n_files)}"
834831
if blockers:
835832
msg += " (errors prevented further checking)"

test-data/unit/cmdline-based-baseline.test

+54
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
a
1010
[out]
1111
pkg/a.py:1:1: error: Name "a" is not defined [name-defined]
12+
13+
baseline:
14+
1215
Found 1 error (1 new error) in 1 file (checked 1 source file)
1316
Baseline successfully written to a/b
1417
== Return code: 0
@@ -602,3 +605,54 @@ a
602605
"file:a.py"
603606
]
604607
}
608+
609+
610+
[case testBaselineAllow]
611+
# cmd: mypy --write-baseline --baseline-allow=operator a.py
612+
[file a.py]
613+
a
614+
1 + ""
615+
[out]
616+
a.py:1:1: error: Name "a" is not defined [name-defined]
617+
a.py:2:5: error: Unsupported operand types for + ("int" and "str") [operator]
618+
619+
620+
621+
[case testBaselineAllowUpdate]
622+
# cmd: mypy --write-baseline --baseline-allow=operator a.py
623+
[file a.py]
624+
a
625+
1 + ""
626+
[file .mypy/baseline.json]
627+
{
628+
"files": {
629+
"a.py": [
630+
{
631+
"code": "name-defined",
632+
"column": 0,
633+
"message": "Name \"a\" is not defined",
634+
"offset": 1,
635+
"src": "a",
636+
"target": "a"
637+
}
638+
]
639+
},
640+
"format": "1.7",
641+
"targets": [
642+
"file:a.py"
643+
]
644+
}
645+
[out]
646+
a.py:2:5: error: Unsupported operand types for + ("int" and "str") [operator]
647+
Baseline successfully written to .mypy/baseline.json
648+
649+
650+
[case testBaselineBan]
651+
# cmd: mypy --write-baseline --baseline-ban operator a.py
652+
[file a.py]
653+
a
654+
1 + ""
655+
[out]
656+
a.py:1:1: error: Unsupported operand types for + ("int" and "str") [name-defined]
657+
a.py:2:5: error: Unsupported operand types for + ("int" and "str") [operator]
658+
Baseline successfully written to .mypy/baseline.json

0 commit comments

Comments
 (0)