Skip to content

Commit 3774d39

Browse files
committed
Merge branch 'master' into typeddict-readonly
2 parents 01f2e4b + 8332c6e commit 3774d39

File tree

16 files changed

+309
-32
lines changed

16 files changed

+309
-32
lines changed

docs/source/error_code_list.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,7 @@ comment:
11841184

11851185
.. code-block:: python
11861186
1187-
class MyClass
1187+
class MyClass:
11881188
@special # type: ignore[prop-decorator]
11891189
@property
11901190
def magic(self) -> str:

mypy/checker.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,11 @@ def check_partial(self, node: DeferredNodeType | FineGrainedDeferredNodeType) ->
540540
self.check_top_level(node)
541541
else:
542542
self.recurse_into_functions = True
543-
if isinstance(node, LambdaExpr):
544-
self.expr_checker.accept(node)
545-
else:
546-
self.accept(node)
543+
with self.binder.top_frame_context():
544+
if isinstance(node, LambdaExpr):
545+
self.expr_checker.accept(node)
546+
else:
547+
self.accept(node)
547548

548549
def check_top_level(self, node: MypyFile) -> None:
549550
"""Check only the top-level of a module, skipping function definitions."""
@@ -682,6 +683,8 @@ def extract_callable_type(self, inner_type: Type | None, ctx: Context) -> Callab
682683
inner_type = get_proper_type(inner_type)
683684
outer_type: CallableType | None = None
684685
if inner_type is not None and not isinstance(inner_type, AnyType):
686+
if isinstance(inner_type, TypeVarLikeType):
687+
inner_type = get_proper_type(inner_type.upper_bound)
685688
if isinstance(inner_type, TypeType):
686689
if isinstance(inner_type.item, Instance):
687690
inner_type = expand_type_by_instance(

mypy/fastparse.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,10 @@ def parse_type_string(
331331
"""
332332
try:
333333
_, node = parse_type_comment(f"({expr_string})", line=line, column=column, errors=None)
334-
if isinstance(node, UnboundType) and node.original_str_expr is None:
334+
if isinstance(node, (UnboundType, UnionType)) and node.original_str_expr is None:
335335
node.original_str_expr = expr_string
336336
node.original_str_fallback = expr_fallback_name
337337
return node
338-
elif isinstance(node, UnionType):
339-
return node
340338
else:
341339
return RawExpressionType(expr_string, expr_fallback_name, line, column)
342340
except (SyntaxError, ValueError):

mypy/main.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from mypy.find_sources import InvalidSourceList, create_source_list
2424
from mypy.fscache import FileSystemCache
2525
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, get_search_dirs, mypy_path
26-
from mypy.options import COMPLETE_FEATURES, INCOMPLETE_FEATURES, BuildType, Options
26+
from mypy.options import INCOMPLETE_FEATURES, BuildType, Options
2727
from mypy.split_namespace import SplitNamespace
2828
from mypy.version import __version__
2929

@@ -1336,13 +1336,7 @@ def set_strict_flags() -> None:
13361336
validate_package_allow_list(options.untyped_calls_exclude)
13371337

13381338
options.process_error_codes(error_callback=parser.error)
1339-
1340-
# Validate incomplete features.
1341-
for feature in options.enable_incomplete_feature:
1342-
if feature not in INCOMPLETE_FEATURES | COMPLETE_FEATURES:
1343-
parser.error(f"Unknown incomplete feature: {feature}")
1344-
if feature in COMPLETE_FEATURES:
1345-
print(f"Warning: {feature} is already enabled by default")
1339+
options.process_incomplete_features(error_callback=parser.error, warning_callback=print)
13461340

13471341
# Compute absolute path for custom typeshed (if present).
13481342
if options.custom_typeshed_dir is not None:

mypy/options.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,16 @@ def process_error_codes(self, *, error_callback: Callable[[str], Any]) -> None:
433433
# Enabling an error code always overrides disabling
434434
self.disabled_error_codes -= self.enabled_error_codes
435435

436+
def process_incomplete_features(
437+
self, *, error_callback: Callable[[str], Any], warning_callback: Callable[[str], Any]
438+
) -> None:
439+
# Validate incomplete features.
440+
for feature in self.enable_incomplete_feature:
441+
if feature not in INCOMPLETE_FEATURES | COMPLETE_FEATURES:
442+
error_callback(f"Unknown incomplete feature: {feature}")
443+
if feature in COMPLETE_FEATURES:
444+
warning_callback(f"Warning: {feature} is already enabled by default")
445+
436446
def apply_changes(self, changes: dict[str, object]) -> Options:
437447
# Note: effects of this method *must* be idempotent.
438448
new_options = Options()

mypy/stubgen.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import mypy.parse
5555
import mypy.traverser
5656
import mypy.util
57+
import mypy.version
5758
from mypy.build import build
5859
from mypy.errors import CompileError, Errors
5960
from mypy.find_sources import InvalidSourceList, create_source_list
@@ -304,9 +305,26 @@ def visit_name_expr(self, node: NameExpr) -> str:
304305
def visit_member_expr(self, o: MemberExpr) -> str:
305306
return self._visit_ref_expr(o)
306307

307-
def visit_str_expr(self, node: StrExpr) -> str:
308+
def _visit_literal_node(
309+
self, node: StrExpr | BytesExpr | IntExpr | FloatExpr | ComplexExpr
310+
) -> str:
308311
return repr(node.value)
309312

313+
def visit_str_expr(self, node: StrExpr) -> str:
314+
return self._visit_literal_node(node)
315+
316+
def visit_bytes_expr(self, node: BytesExpr) -> str:
317+
return f"b{self._visit_literal_node(node)}"
318+
319+
def visit_int_expr(self, node: IntExpr) -> str:
320+
return self._visit_literal_node(node)
321+
322+
def visit_float_expr(self, node: FloatExpr) -> str:
323+
return self._visit_literal_node(node)
324+
325+
def visit_complex_expr(self, node: ComplexExpr) -> str:
326+
return self._visit_literal_node(node)
327+
310328
def visit_index_expr(self, node: IndexExpr) -> str:
311329
base_fullname = self.stubgen.get_fullname(node.base)
312330
if base_fullname == "typing.Union":
@@ -804,7 +822,8 @@ def get_base_types(self, cdef: ClassDef) -> list[str]:
804822
for name, value in cdef.keywords.items():
805823
if name == "metaclass":
806824
continue # handled separately
807-
base_types.append(f"{name}={value.accept(p)}")
825+
processed_value = value.accept(p) or "..." # at least, don't crash
826+
base_types.append(f"{name}={processed_value}")
808827
return base_types
809828

810829
def get_class_decorators(self, cdef: ClassDef) -> list[str]:
@@ -1847,6 +1866,9 @@ def parse_options(args: list[str]) -> Options:
18471866
dest="files",
18481867
help="generate stubs for given files or directories",
18491868
)
1869+
parser.add_argument(
1870+
"--version", action="version", version="%(prog)s " + mypy.version.__version__
1871+
)
18501872

18511873
ns = parser.parse_args(args)
18521874

mypy/stubtest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,9 @@ def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[s
12241224
if isinstance(runtime, property):
12251225
yield from _verify_final_method(stub.func, runtime.fget, MISSING)
12261226
return
1227+
if isinstance(runtime, functools.cached_property):
1228+
yield from _verify_final_method(stub.func, runtime.func, MISSING)
1229+
return
12271230
if inspect.isdatadescriptor(runtime):
12281231
# It's enough like a property...
12291232
return
@@ -1945,7 +1948,13 @@ def error_callback(msg: str) -> typing.NoReturn:
19451948
print(_style("error:", color="red", bold=True), msg)
19461949
sys.exit(1)
19471950

1951+
def warning_callback(msg: str) -> None:
1952+
print(_style("warning:", color="yellow", bold=True), msg)
1953+
19481954
options.process_error_codes(error_callback=error_callback)
1955+
options.process_incomplete_features(
1956+
error_callback=error_callback, warning_callback=warning_callback
1957+
)
19491958

19501959
try:
19511960
modules = build_stubs(modules, options, find_submodules=not args.check_typeshed)

mypy/test/teststubtest.py

Lines changed: 148 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ def __invert__(self: _T) -> _T: pass
144144
"""
145145

146146

147-
def run_stubtest(
147+
def run_stubtest_with_stderr(
148148
stub: str, runtime: str, options: list[str], config_file: str | None = None
149-
) -> str:
149+
) -> tuple[str, str]:
150150
with use_tmp_dir(TEST_MODULE_NAME) as tmp_dir:
151151
with open("builtins.pyi", "w") as f:
152152
f.write(stubtest_builtins_stub)
@@ -163,13 +163,26 @@ def run_stubtest(
163163
f.write(config_file)
164164
options = options + ["--mypy-config-file", f"{TEST_MODULE_NAME}_config.ini"]
165165
output = io.StringIO()
166-
with contextlib.redirect_stdout(output):
166+
outerr = io.StringIO()
167+
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(outerr):
167168
test_stubs(parse_options([TEST_MODULE_NAME] + options), use_builtins_fixtures=True)
168-
return remove_color_code(
169-
output.getvalue()
170-
# remove cwd as it's not available from outside
171-
.replace(os.path.realpath(tmp_dir) + os.sep, "").replace(tmp_dir + os.sep, "")
172-
)
169+
filtered_output = remove_color_code(
170+
output.getvalue()
171+
# remove cwd as it's not available from outside
172+
.replace(os.path.realpath(tmp_dir) + os.sep, "").replace(tmp_dir + os.sep, "")
173+
)
174+
filtered_outerr = remove_color_code(
175+
outerr.getvalue()
176+
# remove cwd as it's not available from outside
177+
.replace(os.path.realpath(tmp_dir) + os.sep, "").replace(tmp_dir + os.sep, "")
178+
)
179+
return filtered_output, filtered_outerr
180+
181+
182+
def run_stubtest(
183+
stub: str, runtime: str, options: list[str], config_file: str | None = None
184+
) -> str:
185+
return run_stubtest_with_stderr(stub, runtime, options, config_file)[0]
173186

174187

175188
class Case:
@@ -893,6 +906,106 @@ class FineAndDandy:
893906
error=None,
894907
)
895908

909+
@collect_cases
910+
def test_cached_property(self) -> Iterator[Case]:
911+
yield Case(
912+
stub="""
913+
from functools import cached_property
914+
class Good:
915+
@cached_property
916+
def read_only_attr(self) -> int: ...
917+
@cached_property
918+
def read_only_attr2(self) -> int: ...
919+
""",
920+
runtime="""
921+
import functools as ft
922+
from functools import cached_property
923+
class Good:
924+
@cached_property
925+
def read_only_attr(self): return 1
926+
@ft.cached_property
927+
def read_only_attr2(self): return 1
928+
""",
929+
error=None,
930+
)
931+
yield Case(
932+
stub="""
933+
from functools import cached_property
934+
class Bad:
935+
@cached_property
936+
def f(self) -> int: ...
937+
""",
938+
runtime="""
939+
class Bad:
940+
def f(self) -> int: return 1
941+
""",
942+
error="Bad.f",
943+
)
944+
yield Case(
945+
stub="""
946+
from functools import cached_property
947+
class GoodCachedAttr:
948+
@cached_property
949+
def f(self) -> int: ...
950+
""",
951+
runtime="""
952+
class GoodCachedAttr:
953+
f = 1
954+
""",
955+
error=None,
956+
)
957+
yield Case(
958+
stub="""
959+
from functools import cached_property
960+
class BadCachedAttr:
961+
@cached_property
962+
def f(self) -> str: ...
963+
""",
964+
runtime="""
965+
class BadCachedAttr:
966+
f = 1
967+
""",
968+
error="BadCachedAttr.f",
969+
)
970+
yield Case(
971+
stub="""
972+
from functools import cached_property
973+
from typing import final
974+
class FinalGood:
975+
@cached_property
976+
@final
977+
def attr(self) -> int: ...
978+
""",
979+
runtime="""
980+
from functools import cached_property
981+
from typing import final
982+
class FinalGood:
983+
@cached_property
984+
@final
985+
def attr(self):
986+
return 1
987+
""",
988+
error=None,
989+
)
990+
yield Case(
991+
stub="""
992+
from functools import cached_property
993+
class FinalBad:
994+
@cached_property
995+
def attr(self) -> int: ...
996+
""",
997+
runtime="""
998+
from functools import cached_property
999+
from typing_extensions import final
1000+
class FinalBad:
1001+
@cached_property
1002+
@final
1003+
def attr(self):
1004+
return 1
1005+
""",
1006+
error="FinalBad.attr",
1007+
)
1008+
8961009
@collect_cases
8971010
def test_var(self) -> Iterator[Case]:
8981011
yield Case(stub="x1: int", runtime="x1 = 5", error=None)
@@ -2490,6 +2603,33 @@ def test_config_file_error_codes(self) -> None:
24902603
output = run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file)
24912604
assert output == "Success: no issues found in 1 module\n"
24922605

2606+
def test_config_file_error_codes_invalid(self) -> None:
2607+
runtime = "temp = 5\n"
2608+
stub = "temp: int\n"
2609+
config_file = "[mypy]\ndisable_error_code = not-a-valid-name\n"
2610+
output, outerr = run_stubtest_with_stderr(
2611+
stub=stub, runtime=runtime, options=[], config_file=config_file
2612+
)
2613+
assert output == "Success: no issues found in 1 module\n"
2614+
assert outerr == (
2615+
"test_module_config.ini: [mypy]: disable_error_code: "
2616+
"Invalid error code(s): not-a-valid-name\n"
2617+
)
2618+
2619+
def test_config_file_wrong_incomplete_feature(self) -> None:
2620+
runtime = "x = 1\n"
2621+
stub = "x: int\n"
2622+
config_file = "[mypy]\nenable_incomplete_feature = Unpack\n"
2623+
output = run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file)
2624+
assert output == (
2625+
"warning: Warning: Unpack is already enabled by default\n"
2626+
"Success: no issues found in 1 module\n"
2627+
)
2628+
2629+
config_file = "[mypy]\nenable_incomplete_feature = not-a-valid-name\n"
2630+
with self.assertRaises(SystemExit):
2631+
run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file)
2632+
24932633
def test_no_modules(self) -> None:
24942634
output = io.StringIO()
24952635
with contextlib.redirect_stdout(output):

mypy/typeanal.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1641,7 +1641,11 @@ def analyze_literal_type(self, t: UnboundType) -> Type:
16411641

16421642
def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type] | None:
16431643
# This UnboundType was originally defined as a string.
1644-
if isinstance(arg, UnboundType) and arg.original_str_expr is not None:
1644+
if (
1645+
isinstance(arg, ProperType)
1646+
and isinstance(arg, (UnboundType, UnionType))
1647+
and arg.original_str_expr is not None
1648+
):
16451649
assert arg.original_str_fallback is not None
16461650
return [
16471651
LiteralType(

mypy/typeops.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,14 @@ class B(A): pass
310310
if not func.arg_types:
311311
# Invalid method, return something.
312312
return cast(F, func)
313-
if func.arg_kinds[0] == ARG_STAR:
313+
if func.arg_kinds[0] in (ARG_STAR, ARG_STAR2):
314314
# The signature is of the form 'def foo(*args, ...)'.
315315
# In this case we shouldn't drop the first arg,
316316
# since func will be absorbed by the *args.
317-
318317
# TODO: infer bounds on the type of *args?
318+
319+
# In the case of **kwargs we should probably emit an error, but
320+
# for now we simply skip it, to avoid crashes down the line.
319321
return cast(F, func)
320322
self_param_type = get_proper_type(func.arg_types[0])
321323

0 commit comments

Comments
 (0)