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

mypy/config_parser.py

Lines changed: 2 additions & 0 deletions
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,

mypy/main.py

Lines changed: 19 additions & 1 deletion
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)

mypy/options.py

Lines changed: 7 additions & 1 deletion
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 --

mypy/semanal.py

Lines changed: 12 additions & 3 deletions
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)

mypy/semanal_shared.py

Lines changed: 4 additions & 0 deletions
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

mypy/test/testcheck.py

Lines changed: 3 additions & 1 deletion
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.

mypy/test/testfinegrained.py

Lines changed: 2 additions & 2 deletions
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:

mypy/test/testsemanal.py

Lines changed: 2 additions & 2 deletions
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

mypy/test/testtransform.py

Lines changed: 2 additions & 1 deletion
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

mypy/typeanal.py

Lines changed: 2 additions & 4 deletions
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

0 commit comments

Comments
 (0)