Skip to content

Commit

Permalink
baseline allow
Browse files Browse the repository at this point in the history
  • Loading branch information
KotlinIsland committed Jul 21, 2024
1 parent 547f363 commit 18a17fb
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Enable stub mode within `TYPE_CHECKING` branches (#702)
- Infer from overloads - add default value in impl (#697)
- Warn for missing returns with explicit `Any` return types (#715)
- `--baseline-allow` and `--baseline-ban` for baseline management (#710)
### Fixes
- positional arguments on overloads break super (#697)
- positional arguments on overloads duplicate unions (#697)
Expand Down
10 changes: 5 additions & 5 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
module_prefix,
read_py_file,
time_ref,
time_spent_us,
time_spent_us, plural_s,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -1073,14 +1073,14 @@ def save_baseline(manager: BuildManager):
# Indicate that writing was canceled
manager.options.write_baseline = False
return
new_baseline = manager.errors.prepare_baseline_errors()
new_baseline, rejected = manager.errors.prepare_baseline_errors()
file = Path(manager.options.baseline_file)
if not new_baseline:
if file.exists():
file.unlink()
print("No errors, baseline file removed")
print("No accepted errors, baseline file removed")
elif manager.options.write_baseline:
print("No errors, no baseline to write")
print("No accepted errors, no baseline to write")
# Indicate that writing was canceled
manager.options.write_baseline = False
return
Expand All @@ -1096,7 +1096,7 @@ def save_baseline(manager: BuildManager):
with file.open("w") as f:
json.dump(data, f, indent=2, sort_keys=True)
if not manager.options.write_baseline and manager.options.auto_baseline:
manager.stdout.write(f"Baseline successfully updated at {file}\n")
manager.stdout.write(f"{removed} error{plural_s(removed)} from baseline} {file}\n")


def load_baseline(options: Options, errors: Errors, stdout: TextIO) -> None:
Expand Down
28 changes: 26 additions & 2 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1359,7 +1359,9 @@ def initialize_baseline(
self.baseline = baseline_errors
self.baseline_targets = targets

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

def remove_duplicates(errors: list[ErrorInfo]) -> list[ErrorInfo]:
Expand All @@ -1383,6 +1385,25 @@ def remove_duplicates(errors: list[ErrorInfo]) -> list[ErrorInfo]:
i += 1
return unduplicated_result

allowed = self.options.baseline_allows
banned = self.options.baseline_allows
rejected = []
error_list = []

def gate_keep(error: ErrorInfo) -> bool:
"""should an error be accepted into the baseline"""

def yes_no(condition: bool) -> bool:
if error.severity == "error":
(error_list if condition else rejected).append(error)
return condition

if allowed:
return yes_no(error.code in allowed)
if banned:
return yes_no(error.code in banned)
return yes_no(True)

result = {
self.common_path(file): [
{
Expand All @@ -1396,18 +1417,21 @@ def remove_duplicates(errors: list[ErrorInfo]) -> list[ErrorInfo]:
and cast(List[str], self.read_source(file))[error.line - 1].strip(),
}
for error in remove_duplicates(errors)
if gate_keep(error)
# don't store reveal errors
if error.code != codes.REVEAL
]
for file, errors in self.all_errors.items()
}
result = {file: errors for file, errors in result.items() if errors}
for file in result.values():
previous = 0
for error in file:
error["offset"] = cast(int, error["line"]) - previous
previous = cast(int, error["line"])
del error["line"]
return cast(Dict[str, List[StoredBaselineError]], result)
self.baseline_stats = {"total": error_list, "rejected": len(rejected)}
return cast(Dict[str, List[StoredBaselineError]], result), rejected

def filter_baseline(
self, errors: list[ErrorInfo], path: str, source_lines: list[str] | None
Expand Down
75 changes: 51 additions & 24 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, get_search_dirs, mypy_path
from mypy.options import COMPLETE_FEATURES, INCOMPLETE_FEATURES, BuildType, Options
from mypy.split_namespace import SplitNamespace
from mypy.util import plural_s
from mypy.version import __based_version__, __version__

orig_stat: Final = os.stat
Expand Down Expand Up @@ -122,41 +123,45 @@ def main(
if messages and n_notes < len(messages):
code = 2 if blockers else 1
if options.error_summary:
if options.summary and res and n_errors:
stdout.write("\n")
stdout.write(formatter.style("error summary:\n", color="none", bold=True))
all_errors = defaultdict(lambda: 0)
for errors in res.manager.errors.error_info_map.values():
for error in errors:
if error.severity != "error" or not error.code:
continue
all_errors[error.code.code] += 1
if all_errors:
max_code_name_length = max(len(code_name) for code_name in all_errors)
for code_name, count in all_errors.items():
stdout.write(f" {code_name:<{max_code_name_length}} {count:>5}\n")
if options.write_baseline and res:
new_errors = n_errors
n_files = len(res.manager.errors.all_errors)
total = []
# This is stupid, but it's just to remove the dupes from the unfiltered errors
for errors in res.manager.errors.all_errors.values():
temp = res.manager.errors.render_messages(errors)
total.extend(res.manager.errors.remove_duplicates(temp))
n_errors = len([error for error in total if error[5] == "error"])
else:
new_errors = -1
stats = res.manager.errors.baseline_stats
rejected = stats["rejected"]
total = len(stats["total"])
new_errors = n_errors - rejected
previous = res.manager.errors.original_baseline
removed = len([error for file in previous.values() for error in file])
stdout.write(formatter.style("baseline:\n", color="none", bold=True))
stdout.write(f" {new_errors} new error{plural_s(new_errors)}\n")
stdout.write(f" {total} error{plural_s(total)} in baseline\n")
if removed:
stdout.write(f" {removed} error{plural_s(removed)} from previous baseline\n")
stdout.write(f" {rejected} error{plural_s(rejected)} rejected\n")
stdout.write(f" baseline successfully written to {options.baseline_file}\n")
stdout.flush()
if n_errors:
summary = formatter.format_error(
n_errors,
n_files,
len(sources),
new_errors=new_errors,
blockers=blockers,
use_color=options.color_output,
n_errors, n_files, len(sources), blockers=blockers, use_color=options.color_output
)
stdout.write(summary + "\n")
# Only notes should also output success
elif not messages or n_notes == len(messages):
stdout.write(formatter.format_success(len(sources), options.color_output) + "\n")
stdout.flush()

if options.write_baseline:
stdout.write(
formatter.style(
f"Baseline successfully written to {options.baseline_file}\n", "green", bold=True
)
)
stdout.flush()
code = 0

if options.install_types and not options.non_interactive:
result = install_types(formatter, options, after_run=True, non_interactive=False)
if result:
Expand Down Expand Up @@ -563,6 +568,20 @@ def add_invertible_flag(
action="store",
help=f"Use baseline info in the given file (defaults to '{defaults.BASELINE_FILE}')",
)
based_group.add_argument(
"--baseline-allow",
metavar="NAME",
action="append",
default=[],
help="Allow error codes into the baseline",
)
based_group.add_argument(
"--baseline-ban",
metavar="NAME",
action="append",
default=[],
help="Prevent error codes from being written to the baseline",
)
add_invertible_flag(
"--no-auto-baseline",
default=True,
Expand Down Expand Up @@ -619,6 +638,12 @@ def add_invertible_flag(
"You probably want to set this on a module override",
group=based_group,
)
add_invertible_flag(
"--no-summary",
default=False,
help="don't show an error code summary at the end",
group=based_group,
)
add_invertible_flag(
"--ide", default=False, help="Best default for IDE integration.", group=based_group
)
Expand Down Expand Up @@ -1460,6 +1485,8 @@ def set_ide_flags() -> None:
if invalid_codes:
parser.error(f"Invalid error code(s): {', '.join(sorted(invalid_codes))}")

options.baseline_bans |= {error_codes[code] for code in set(options.baseline_ban)}
options.baseline_allows |= {error_codes[code] for code in set(options.baseline_allow)}
options.disabled_error_codes |= {error_codes[code] for code in disabled_codes}
options.enabled_error_codes |= {error_codes[code] for code in enabled_codes}

Expand Down
12 changes: 11 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ def __init__(self) -> None:
# Based options
self.write_baseline = False
self.baseline_file = defaults.BASELINE_FILE
self.baseline_allow: list[str] = []
self.baseline_allows: set[ErrorCode] = set()
self.baseline_ban: list[str] = []
self.baseline_bans: set[ErrorCode] = set()
self.summary = True
self.auto_baseline = True
self.default_return = True
self.infer_function_types = True
Expand Down Expand Up @@ -609,7 +614,12 @@ def select_options_affecting_cache(self) -> Mapping[str, object]:
result: dict[str, object] = {}
for opt in OPTIONS_AFFECTING_CACHE:
val = getattr(self, opt)
if opt in ("disabled_error_codes", "enabled_error_codes"):
if opt in (
"disabled_error_codes",
"enabled_error_codes",
"baseline_allows",
"baseline_bans",
):
val = sorted([code.code for code in val])
result[opt] = val
return result
Expand Down
3 changes: 0 additions & 3 deletions mypy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,12 +824,9 @@ def format_error(
*,
blockers: bool = False,
use_color: bool = True,
new_errors: int = -1,
) -> str:
"""Format a short summary in case of errors."""
msg = f"Found {n_errors} error{plural_s(n_errors)} "
if new_errors != -1:
msg += f"({new_errors} new error{plural_s(new_errors)}) "
msg += f"in {n_files} file{plural_s(n_files)}"
if blockers:
msg += " (errors prevented further checking)"
Expand Down
54 changes: 54 additions & 0 deletions test-data/unit/cmdline-based-baseline.test
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
a
[out]
pkg/a.py:1:1: error: Name "a" is not defined [name-defined]

baseline:

Found 1 error (1 new error) in 1 file (checked 1 source file)
Baseline successfully written to a/b
== Return code: 0
Expand Down Expand Up @@ -602,3 +605,54 @@ a
"file:a.py"
]
}


[case testBaselineAllow]
# cmd: mypy --write-baseline --baseline-allow=operator a.py
[file a.py]
a
1 + ""
[out]
a.py:1:1: error: Name "a" is not defined [name-defined]
a.py:2:5: error: Unsupported operand types for + ("int" and "str") [operator]



[case testBaselineAllowUpdate]
# cmd: mypy --write-baseline --baseline-allow=operator a.py
[file a.py]
a
1 + ""
[file .mypy/baseline.json]
{
"files": {
"a.py": [
{
"code": "name-defined",
"column": 0,
"message": "Name \"a\" is not defined",
"offset": 1,
"src": "a",
"target": "a"
}
]
},
"format": "1.7",
"targets": [
"file:a.py"
]
}
[out]
a.py:2:5: error: Unsupported operand types for + ("int" and "str") [operator]
Baseline successfully written to .mypy/baseline.json


[case testBaselineBan]
# cmd: mypy --write-baseline --baseline-ban operator a.py
[file a.py]
a
1 + ""
[out]
a.py:1:1: error: Unsupported operand types for + ("int" and "str") [name-defined]
a.py:2:5: error: Unsupported operand types for + ("int" and "str") [operator]
Baseline successfully written to .mypy/baseline.json

0 comments on commit 18a17fb

Please sign in to comment.