Skip to content

Commit b79a20a

Browse files
Allow enabling individual experimental features (#13790)
Ref #13685 Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
1 parent edf83f3 commit b79a20a

16 files changed

+75
-21
lines changed

Diff for: mypy/config_parser.py

+2
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def check_follow_imports(choice: str) -> str:
154154
"plugins": lambda s: [p.strip() for p in s.split(",")],
155155
"always_true": lambda s: [p.strip() for p in s.split(",")],
156156
"always_false": lambda s: [p.strip() for p in s.split(",")],
157+
"enable_incomplete_feature": lambda s: [p.strip() for p in s.split(",")],
157158
"disable_error_code": lambda s: validate_codes([p.strip() for p in s.split(",")]),
158159
"enable_error_code": lambda s: validate_codes([p.strip() for p in s.split(",")]),
159160
"package_root": lambda s: [p.strip() for p in s.split(",")],
@@ -176,6 +177,7 @@ def check_follow_imports(choice: str) -> str:
176177
"plugins": try_split,
177178
"always_true": try_split,
178179
"always_false": try_split,
180+
"enable_incomplete_feature": try_split,
179181
"disable_error_code": lambda s: validate_codes(try_split(s)),
180182
"enable_error_code": lambda s: validate_codes(try_split(s)),
181183
"package_root": try_split,

Diff for: mypy/main.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from mypy.find_sources import InvalidSourceList, create_source_list
1919
from mypy.fscache import FileSystemCache
2020
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, get_search_dirs, mypy_path
21-
from mypy.options import BuildType, Options
21+
from mypy.options import INCOMPLETE_FEATURES, BuildType, Options
2222
from mypy.split_namespace import SplitNamespace
2323
from mypy.version import __version__
2424

@@ -979,6 +979,12 @@ def add_invertible_flag(
979979
action="store_true",
980980
help="Disable experimental support for recursive type aliases",
981981
)
982+
parser.add_argument(
983+
"--enable-incomplete-feature",
984+
action="append",
985+
metavar="FEATURE",
986+
help="Enable support of incomplete/experimental features for early preview",
987+
)
982988
internals_group.add_argument(
983989
"--custom-typeshed-dir", metavar="DIR", help="Use the custom typeshed in DIR"
984990
)
@@ -1107,6 +1113,7 @@ def add_invertible_flag(
11071113
parser.add_argument(
11081114
"--cache-map", nargs="+", dest="special-opts:cache_map", help=argparse.SUPPRESS
11091115
)
1116+
# This one is deprecated, but we will keep it for few releases.
11101117
parser.add_argument(
11111118
"--enable-incomplete-features", action="store_true", help=argparse.SUPPRESS
11121119
)
@@ -1274,6 +1281,17 @@ def set_strict_flags() -> None:
12741281
# Enabling an error code always overrides disabling
12751282
options.disabled_error_codes -= options.enabled_error_codes
12761283

1284+
# Validate incomplete features.
1285+
for feature in options.enable_incomplete_feature:
1286+
if feature not in INCOMPLETE_FEATURES:
1287+
parser.error(f"Unknown incomplete feature: {feature}")
1288+
if options.enable_incomplete_features:
1289+
print(
1290+
"Warning: --enable-incomplete-features is deprecated, use"
1291+
" --enable-incomplete-feature=FEATURE instead"
1292+
)
1293+
options.enable_incomplete_feature = list(INCOMPLETE_FEATURES)
1294+
12771295
# Compute absolute path for custom typeshed (if present).
12781296
if options.custom_typeshed_dir is not None:
12791297
options.abs_custom_typeshed_dir = os.path.abspath(options.custom_typeshed_dir)

Diff for: mypy/options.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ class BuildType:
6060
"debug_cache"
6161
}
6262

63+
# Features that are currently incomplete/experimental
64+
TYPE_VAR_TUPLE: Final = "TypeVarTuple"
65+
UNPACK: Final = "Unpack"
66+
INCOMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK))
67+
6368

6469
class Options:
6570
"""Options collected from flags."""
@@ -268,7 +273,8 @@ def __init__(self) -> None:
268273
self.dump_type_stats = False
269274
self.dump_inference_stats = False
270275
self.dump_build_stats = False
271-
self.enable_incomplete_features = False
276+
self.enable_incomplete_features = False # deprecated
277+
self.enable_incomplete_feature: list[str] = []
272278
self.timing_stats: str | None = None
273279

274280
# -- test options --

Diff for: mypy/semanal.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@
178178
type_aliases_source_versions,
179179
typing_extensions_aliases,
180180
)
181-
from mypy.options import Options
181+
from mypy.options import TYPE_VAR_TUPLE, Options
182182
from mypy.patterns import (
183183
AsPattern,
184184
ClassPattern,
@@ -3911,8 +3911,7 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
39113911
if len(call.args) > 1:
39123912
self.fail("Only the first argument to TypeVarTuple has defined semantics", s)
39133913

3914-
if not self.options.enable_incomplete_features:
3915-
self.fail('"TypeVarTuple" is not supported by mypy yet', s)
3914+
if not self.incomplete_feature_enabled(TYPE_VAR_TUPLE, s):
39163915
return False
39173916

39183917
name = self.extract_typevarlike_name(s, call)
@@ -5973,6 +5972,16 @@ def note(self, msg: str, ctx: Context, code: ErrorCode | None = None) -> None:
59735972
return
59745973
self.errors.report(ctx.get_line(), ctx.get_column(), msg, severity="note", code=code)
59755974

5975+
def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool:
5976+
if feature not in self.options.enable_incomplete_feature:
5977+
self.fail(
5978+
f'"{feature}" support is experimental,'
5979+
f" use --enable-incomplete-feature={feature} to enable",
5980+
ctx,
5981+
)
5982+
return False
5983+
return True
5984+
59765985
def accept(self, node: Node) -> None:
59775986
try:
59785987
node.accept(self)

Diff for: mypy/semanal_shared.py

+4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def fail(
8282
def note(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
8383
raise NotImplementedError
8484

85+
@abstractmethod
86+
def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool:
87+
raise NotImplementedError
88+
8589
@abstractmethod
8690
def record_incomplete_ref(self) -> None:
8791
raise NotImplementedError

Diff for: mypy/test/testcheck.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from mypy.build import Graph
1111
from mypy.errors import CompileError
1212
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths
13+
from mypy.options import TYPE_VAR_TUPLE, UNPACK
1314
from mypy.semanal_main import core_modules
1415
from mypy.test.config import test_data_prefix, test_temp_dir
1516
from mypy.test.data import DataDrivenTestCase, DataSuite, FileOperation, module_from_path
@@ -110,7 +111,8 @@ def run_case_once(
110111
# Parse options after moving files (in case mypy.ini is being moved).
111112
options = parse_options(original_program_text, testcase, incremental_step)
112113
options.use_builtins_fixtures = True
113-
options.enable_incomplete_features = True
114+
if not testcase.name.endswith("_no_incomplete"):
115+
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
114116
options.show_traceback = True
115117

116118
# Enable some options automatically based on test file name.

Diff for: mypy/test/testfinegrained.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from mypy.errors import CompileError
3030
from mypy.find_sources import create_source_list
3131
from mypy.modulefinder import BuildSource
32-
from mypy.options import Options
32+
from mypy.options import TYPE_VAR_TUPLE, UNPACK, Options
3333
from mypy.server.mergecheck import check_consistency
3434
from mypy.server.update import sort_messages_preserving_file_order
3535
from mypy.test.config import test_temp_dir
@@ -153,7 +153,7 @@ def get_options(self, source: str, testcase: DataDrivenTestCase, build_cache: bo
153153
options.use_fine_grained_cache = self.use_cache and not build_cache
154154
options.cache_fine_grained = self.use_cache
155155
options.local_partial_types = True
156-
options.enable_incomplete_features = True
156+
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
157157
# Treat empty bodies safely for these test cases.
158158
options.allow_empty_bodies = not testcase.name.endswith("_no_empty")
159159
if re.search("flags:.*--follow-imports", source) is None:

Diff for: mypy/test/testsemanal.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from mypy.errors import CompileError
1212
from mypy.modulefinder import BuildSource
1313
from mypy.nodes import TypeInfo
14-
from mypy.options import Options
14+
from mypy.options import TYPE_VAR_TUPLE, UNPACK, Options
1515
from mypy.test.config import test_temp_dir
1616
from mypy.test.data import DataDrivenTestCase, DataSuite
1717
from mypy.test.helpers import (
@@ -46,7 +46,7 @@ def get_semanal_options(program_text: str, testcase: DataDrivenTestCase) -> Opti
4646
options.semantic_analysis_only = True
4747
options.show_traceback = True
4848
options.python_version = PYTHON3_VERSION
49-
options.enable_incomplete_features = True
49+
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
5050
return options
5151

5252

Diff for: mypy/test/testtransform.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mypy import build
88
from mypy.errors import CompileError
99
from mypy.modulefinder import BuildSource
10+
from mypy.options import TYPE_VAR_TUPLE, UNPACK
1011
from mypy.test.config import test_temp_dir
1112
from mypy.test.data import DataDrivenTestCase, DataSuite
1213
from mypy.test.helpers import assert_string_arrays_equal, normalize_error_messages, parse_options
@@ -39,7 +40,7 @@ def test_transform(testcase: DataDrivenTestCase) -> None:
3940
options = parse_options(src, testcase, 1)
4041
options.use_builtins_fixtures = True
4142
options.semantic_analysis_only = True
42-
options.enable_incomplete_features = True
43+
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
4344
options.show_traceback = True
4445
result = build.build(
4546
sources=[BuildSource("main", None, src)], options=options, alt_lib_path=test_temp_dir

Diff for: mypy/typeanal.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
check_arg_names,
3939
get_nongen_builtins,
4040
)
41-
from mypy.options import Options
41+
from mypy.options import UNPACK, Options
4242
from mypy.plugin import AnalyzeTypeContext, Plugin, TypeAnalyzerPluginInterface
4343
from mypy.semanal_shared import SemanticAnalyzerCoreInterface, paramspec_args, paramspec_kwargs
4444
from mypy.tvar_scope import TypeVarLikeScope
@@ -569,9 +569,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
569569
# In most contexts, TypeGuard[...] acts as an alias for bool (ignoring its args)
570570
return self.named_type("builtins.bool")
571571
elif fullname in ("typing.Unpack", "typing_extensions.Unpack"):
572-
# We don't want people to try to use this yet.
573-
if not self.options.enable_incomplete_features:
574-
self.fail('"Unpack" is not supported yet, use --enable-incomplete-features', t)
572+
if not self.api.incomplete_feature_enabled(UNPACK, t):
575573
return AnyType(TypeOfAny.from_error)
576574
return UnpackType(self.anal_type(t.args[0]), line=t.line, column=t.column)
577575
return None

Diff for: mypyc/test-data/run-functions.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -1242,7 +1242,7 @@ def g() -> None:
12421242

12431243
g()
12441244

1245-
[case testIncompleteFeatureUnpackKwargsCompiled]
1245+
[case testUnpackKwargsCompiled]
12461246
from typing_extensions import Unpack, TypedDict
12471247

12481248
class Person(TypedDict):

Diff for: mypyc/test/test_run.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from mypy import build
1717
from mypy.errors import CompileError
18-
from mypy.options import Options
18+
from mypy.options import TYPE_VAR_TUPLE, UNPACK, Options
1919
from mypy.test.config import test_temp_dir
2020
from mypy.test.data import DataDrivenTestCase
2121
from mypy.test.helpers import assert_module_equivalence, perform_file_operations
@@ -194,8 +194,7 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
194194
options.preserve_asts = True
195195
options.allow_empty_bodies = True
196196
options.incremental = self.separate
197-
if "IncompleteFeature" in testcase.name:
198-
options.enable_incomplete_features = True
197+
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
199198

200199
# Avoid checking modules/packages named 'unchecked', to provide a way
201200
# to test interacting with code we don't have types for.

Diff for: test-data/unit/check-flags.test

+11
Original file line numberDiff line numberDiff line change
@@ -2117,3 +2117,14 @@ x: int = "" # E: Incompatible types in assignment (expression has type "str", v
21172117
[case testHideErrorCodes]
21182118
# flags: --hide-error-codes
21192119
x: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
2120+
2121+
[case testTypeVarTupleDisabled_no_incomplete]
2122+
from typing_extensions import TypeVarTuple
2123+
Ts = TypeVarTuple("Ts") # E: "TypeVarTuple" support is experimental, use --enable-incomplete-feature=TypeVarTuple to enable
2124+
[builtins fixtures/tuple.pyi]
2125+
2126+
[case testTypeVarTupleEnabled_no_incomplete]
2127+
# flags: --enable-incomplete-feature=TypeVarTuple
2128+
from typing_extensions import TypeVarTuple
2129+
Ts = TypeVarTuple("Ts") # OK
2130+
[builtins fixtures/tuple.pyi]

Diff for: test-data/unit/cmdline.test

+3-1
Original file line numberDiff line numberDiff line change
@@ -1419,7 +1419,6 @@ exclude = (?x)(
14191419
[out]
14201420
c/cpkg.py:1: error: "int" not callable
14211421

1422-
14231422
[case testCmdlineTimingStats]
14241423
# cmd: mypy --timing-stats timing.txt .
14251424
[file b/__init__.py]
@@ -1435,6 +1434,9 @@ b\.c \d+
14351434
# cmd: mypy --enable-incomplete-features a.py
14361435
[file a.py]
14371436
pass
1437+
[out]
1438+
Warning: --enable-incomplete-features is deprecated, use --enable-incomplete-feature=FEATURE instead
1439+
== Return code: 0
14381440

14391441
[case testShadowTypingModuleEarlyLoad]
14401442
# cmd: mypy dir

Diff for: test-data/unit/fine-grained.test

-1
Original file line numberDiff line numberDiff line change
@@ -9901,7 +9901,6 @@ x = 0 # Arbitrary change to trigger reprocessing
99019901
a.py:3: note: Revealed type is "Tuple[Literal[1]?, Literal['x']?]"
99029902

99039903
[case testUnpackKwargsUpdateFine]
9904-
# flags: --enable-incomplete-features
99059904
import m
99069905
[file shared.py]
99079906
from typing_extensions import TypedDict

Diff for: test-data/unit/lib-stub/typing_extensions.pyi

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class _SpecialForm:
1010
def __getitem__(self, typeargs: Any) -> Any:
1111
pass
1212

13+
def __call__(self, arg: Any) -> Any:
14+
pass
15+
1316
NamedTuple = 0
1417
Protocol: _SpecialForm = ...
1518
def runtime_checkable(x: _T) -> _T: pass

0 commit comments

Comments
 (0)