Skip to content

stubtest: use allowlist instead of whitelist #9426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 37 additions & 34 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ def _verify_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]:
# It's enough like a property...
return
# Sometimes attributes pretend to be properties, for instance, to express that they
# are read only. So whitelist if runtime_type matches the return type of stub.
# are read only. So allowlist if runtime_type matches the return type of stub.
runtime_type = get_mypy_type_of_runtime_value(runtime)
func_type = (
stub.func.type.ret_type if isinstance(stub.func.type, mypy.types.CallableType) else None
Expand Down Expand Up @@ -1001,14 +1001,14 @@ def get_typeshed_stdlib_modules(custom_typeshed_dir: Optional[str]) -> List[str]
return sorted(modules)


def get_whitelist_entries(whitelist_file: str) -> Iterator[str]:
def get_allowlist_entries(allowlist_file: str) -> Iterator[str]:
def strip_comments(s: str) -> str:
try:
return s[: s.index("#")].strip()
except ValueError:
return s.strip()

with open(whitelist_file) as f:
with open(allowlist_file) as f:
for line in f.readlines():
entry = strip_comments(line)
if entry:
Expand All @@ -1017,17 +1017,17 @@ def strip_comments(s: str) -> str:

def test_stubs(args: argparse.Namespace) -> int:
"""This is stubtest! It's time to test the stubs!"""
# Load the whitelist. This is a series of strings corresponding to Error.object_desc
# Values in the dict will store whether we used the whitelist entry or not.
whitelist = {
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
# Values in the dict will store whether we used the allowlist entry or not.
allowlist = {
entry: False
for whitelist_file in args.whitelist
for entry in get_whitelist_entries(whitelist_file)
for allowlist_file in args.allowlist
for entry in get_allowlist_entries(allowlist_file)
}
whitelist_regexes = {entry: re.compile(entry) for entry in whitelist}
allowlist_regexes = {entry: re.compile(entry) for entry in allowlist}

# If we need to generate a whitelist, we store Error.object_desc for each error here.
generated_whitelist = set()
# If we need to generate an allowlist, we store Error.object_desc for each error here.
generated_allowlist = set()

modules = args.modules
if args.check_typeshed:
Expand Down Expand Up @@ -1061,37 +1061,37 @@ def set_strict_flags() -> None: # not needed yet
continue
if args.ignore_positional_only and error.is_positional_only_related():
continue
if error.object_desc in whitelist:
whitelist[error.object_desc] = True
if error.object_desc in allowlist:
allowlist[error.object_desc] = True
continue
is_whitelisted = False
for w in whitelist:
if whitelist_regexes[w].fullmatch(error.object_desc):
whitelist[w] = True
is_whitelisted = True
is_allowlisted = False
for w in allowlist:
if allowlist_regexes[w].fullmatch(error.object_desc):
allowlist[w] = True
is_allowlisted = True
break
if is_whitelisted:
if is_allowlisted:
continue

# We have errors, so change exit code, and output whatever necessary
exit_code = 1
if args.generate_whitelist:
generated_whitelist.add(error.object_desc)
if args.generate_allowlist:
generated_allowlist.add(error.object_desc)
continue
print(error.get_description(concise=args.concise))

# Print unused whitelist entries
if not args.ignore_unused_whitelist:
for w in whitelist:
# Print unused allowlist entries
if not args.ignore_unused_allowlist:
for w in allowlist:
# Don't consider an entry unused if it regex-matches the empty string
# This allows us to whitelist errors that don't manifest at all on some systems
if not whitelist[w] and not whitelist_regexes[w].fullmatch(""):
# This lets us allowlist errors that don't manifest at all on some systems
if not allowlist[w] and not allowlist_regexes[w].fullmatch(""):
exit_code = 1
print("note: unused whitelist entry {}".format(w))
print("note: unused allowlist entry {}".format(w))

# Print the generated whitelist
if args.generate_whitelist:
for e in sorted(generated_whitelist):
# Print the generated allowlist
if args.generate_allowlist:
for e in sorted(generated_allowlist):
print(e)
exit_code = 0

Expand Down Expand Up @@ -1121,24 +1121,27 @@ def parse_options(args: List[str]) -> argparse.Namespace:
"--check-typeshed", action="store_true", help="Check all stdlib modules in typeshed"
)
parser.add_argument(
"--allowlist",
"--whitelist",
action="append",
metavar="FILE",
default=[],
help=(
"Use file as a whitelist. Can be passed multiple times to combine multiple "
"whitelists. Whitelists can be created with --generate-whitelist"
"Use file as an allowlist. Can be passed multiple times to combine multiple "
"allowlists. Allowlists can be created with --generate-allowlist"
),
)
parser.add_argument(
"--generate-allowlist",
"--generate-whitelist",
action="store_true",
help="Print a whitelist (to stdout) to be used with --whitelist",
help="Print an allowlist (to stdout) to be used with --allowlist",
)
parser.add_argument(
"--ignore-unused-allowlist",
"--ignore-unused-whitelist",
action="store_true",
help="Ignore unused whitelist entries",
help="Ignore unused allowlist entries",
)
config_group = parser.add_argument_group(
title='mypy config file',
Expand Down
26 changes: 13 additions & 13 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def test(*args: Any, **kwargs: Any) -> None:
output = run_stubtest(
stub="\n\n".join(textwrap.dedent(c.stub.lstrip("\n")) for c in cases),
runtime="\n\n".join(textwrap.dedent(c.runtime.lstrip("\n")) for c in cases),
options=["--generate-whitelist"],
options=["--generate-allowlist"],
)

actual_errors = set(output.splitlines())
Expand Down Expand Up @@ -667,33 +667,33 @@ def test_ignore_flags(self) -> None:
)
assert not output

def test_whitelist(self) -> None:
def test_allowlist(self) -> None:
# Can't use this as a context because Windows
whitelist = tempfile.NamedTemporaryFile(mode="w+", delete=False)
allowlist = tempfile.NamedTemporaryFile(mode="w+", delete=False)
try:
with whitelist:
whitelist.write("{}.bad # comment\n# comment".format(TEST_MODULE_NAME))
with allowlist:
allowlist.write("{}.bad # comment\n# comment".format(TEST_MODULE_NAME))

output = run_stubtest(
stub="def bad(number: int, text: str) -> None: ...",
runtime="def bad(asdf, text): pass",
options=["--whitelist", whitelist.name],
options=["--allowlist", allowlist.name],
)
assert not output

# test unused entry detection
output = run_stubtest(stub="", runtime="", options=["--whitelist", whitelist.name])
assert output == "note: unused whitelist entry {}.bad\n".format(TEST_MODULE_NAME)
output = run_stubtest(stub="", runtime="", options=["--allowlist", allowlist.name])
assert output == "note: unused allowlist entry {}.bad\n".format(TEST_MODULE_NAME)

output = run_stubtest(
stub="",
runtime="",
options=["--whitelist", whitelist.name, "--ignore-unused-whitelist"],
options=["--allowlist", allowlist.name, "--ignore-unused-allowlist"],
)
assert not output

# test regex matching
with open(whitelist.name, mode="w+") as f:
with open(allowlist.name, mode="w+") as f:
f.write("{}.b.*\n".format(TEST_MODULE_NAME))
f.write("(unused_missing)?\n")
f.write("unused.*\n")
Expand All @@ -713,13 +713,13 @@ def bad(asdf): pass
def also_bad(asdf): pass
""".lstrip("\n")
),
options=["--whitelist", whitelist.name, "--generate-whitelist"],
options=["--allowlist", allowlist.name, "--generate-allowlist"],
)
assert output == "note: unused whitelist entry unused.*\n{}.also_bad\n".format(
assert output == "note: unused allowlist entry unused.*\n{}.also_bad\n".format(
TEST_MODULE_NAME
)
finally:
os.unlink(whitelist.name)
os.unlink(allowlist.name)

def test_mypy_build(self) -> None:
output = run_stubtest(stub="+", runtime="", options=[])
Expand Down