Skip to content
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

Allow enabling individual experimental features #13790

Merged
merged 7 commits into from
Oct 9, 2022
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
2 changes: 2 additions & 0 deletions mypy/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def check_follow_imports(choice: str) -> str:
"plugins": lambda s: [p.strip() for p in s.split(",")],
"always_true": lambda s: [p.strip() for p in s.split(",")],
"always_false": lambda s: [p.strip() for p in s.split(",")],
"enable_incomplete_feature": lambda s: [p.strip() for p in s.split(",")],
"disable_error_code": lambda s: validate_codes([p.strip() for p in s.split(",")]),
"enable_error_code": lambda s: validate_codes([p.strip() for p in s.split(",")]),
"package_root": lambda s: [p.strip() for p in s.split(",")],
Expand All @@ -174,6 +175,7 @@ def check_follow_imports(choice: str) -> str:
"plugins": try_split,
"always_true": try_split,
"always_false": try_split,
"enable_incomplete_feature": try_split,
"disable_error_code": lambda s: validate_codes(try_split(s)),
"enable_error_code": lambda s: validate_codes(try_split(s)),
"package_root": try_split,
Expand Down
20 changes: 19 additions & 1 deletion mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from mypy.find_sources import InvalidSourceList, create_source_list
from mypy.fscache import FileSystemCache
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, get_search_dirs, mypy_path
from mypy.options import BuildType, Options
from mypy.options import INCOMPLETE_FEATURES, BuildType, Options
from mypy.split_namespace import SplitNamespace
from mypy.version import __version__

Expand Down Expand Up @@ -979,6 +979,12 @@ def add_invertible_flag(
action="store_true",
help="Disable experimental support for recursive type aliases",
)
parser.add_argument(
"--enable-incomplete-feature",
action="append",
metavar="FEATURE",
help="Enable support of incomplete/experimental features for early preview",
)
internals_group.add_argument(
"--custom-typeshed-dir", metavar="DIR", help="Use the custom typeshed in DIR"
)
Expand Down Expand Up @@ -1107,6 +1113,7 @@ def add_invertible_flag(
parser.add_argument(
"--cache-map", nargs="+", dest="special-opts:cache_map", help=argparse.SUPPRESS
)
# This one is deprecated, but we will keep it for few releases.
parser.add_argument(
"--enable-incomplete-features", action="store_true", help=argparse.SUPPRESS
)
Expand Down Expand Up @@ -1269,6 +1276,17 @@ def set_strict_flags() -> None:
# Enabling an error code always overrides disabling
options.disabled_error_codes -= options.enabled_error_codes

# Validate incomplete features.
for feature in options.enable_incomplete_feature:
if feature not in INCOMPLETE_FEATURES:
parser.error(f"Unknown incomplete feature: {feature}")
if options.enable_incomplete_features:
print(
"Warning: --enable-incomplete-features is deprecated, use"
" --enable-incomplete-feature=FEATURE instead"
)
options.enable_incomplete_feature = list(INCOMPLETE_FEATURES)

# Compute absolute path for custom typeshed (if present).
if options.custom_typeshed_dir is not None:
options.abs_custom_typeshed_dir = os.path.abspath(options.custom_typeshed_dir)
Expand Down
8 changes: 7 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ class BuildType:
"debug_cache"
}

# Features that are currently incomplete/experimental
TYPE_VAR_TUPLE: Final = "TypeVarTuple"
UNPACK: Final = "Unpack"
INCOMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK))


class Options:
"""Options collected from flags."""
Expand Down Expand Up @@ -262,7 +267,8 @@ def __init__(self) -> None:
self.dump_type_stats = False
self.dump_inference_stats = False
self.dump_build_stats = False
self.enable_incomplete_features = False
self.enable_incomplete_features = False # deprecated
self.enable_incomplete_feature: list[str] = []
self.timing_stats: str | None = None

# -- test options --
Expand Down
15 changes: 12 additions & 3 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
type_aliases_source_versions,
typing_extensions_aliases,
)
from mypy.options import Options
from mypy.options import TYPE_VAR_TUPLE, Options
from mypy.patterns import (
AsPattern,
ClassPattern,
Expand Down Expand Up @@ -3911,8 +3911,7 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
if len(call.args) > 1:
self.fail("Only the first argument to TypeVarTuple has defined semantics", s)

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

name = self.extract_typevarlike_name(s, call)
Expand Down Expand Up @@ -5972,6 +5971,16 @@ def note(self, msg: str, ctx: Context, code: ErrorCode | None = None) -> None:
return
self.errors.report(ctx.get_line(), ctx.get_column(), msg, severity="note", code=code)

def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool:
if feature not in self.options.enable_incomplete_feature:
self.fail(
f'"{feature}" support is experimental,'
f" use --enable-incomplete-feature={feature} to enable",
ctx,
)
return False
return True

def accept(self, node: Node) -> None:
try:
node.accept(self)
Expand Down
4 changes: 4 additions & 0 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def fail(
def note(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
raise NotImplementedError

@abstractmethod
def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool:
raise NotImplementedError

@abstractmethod
def record_incomplete_ref(self) -> None:
raise NotImplementedError
Expand Down
4 changes: 3 additions & 1 deletion mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from mypy.build import Graph
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths
from mypy.options import TYPE_VAR_TUPLE, UNPACK
from mypy.semanal_main import core_modules
from mypy.test.config import test_data_prefix, test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite, FileOperation, module_from_path
Expand Down Expand Up @@ -110,7 +111,8 @@ def run_case_once(
# Parse options after moving files (in case mypy.ini is being moved).
options = parse_options(original_program_text, testcase, incremental_step)
options.use_builtins_fixtures = True
options.enable_incomplete_features = True
if not testcase.name.endswith("_no_incomplete"):
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
options.show_traceback = True

# Enable some options automatically based on test file name.
Expand Down
4 changes: 2 additions & 2 deletions mypy/test/testfinegrained.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from mypy.errors import CompileError
from mypy.find_sources import create_source_list
from mypy.modulefinder import BuildSource
from mypy.options import Options
from mypy.options import TYPE_VAR_TUPLE, UNPACK, Options
from mypy.server.mergecheck import check_consistency
from mypy.server.update import sort_messages_preserving_file_order
from mypy.test.config import test_temp_dir
Expand Down Expand Up @@ -153,7 +153,7 @@ def get_options(self, source: str, testcase: DataDrivenTestCase, build_cache: bo
options.use_fine_grained_cache = self.use_cache and not build_cache
options.cache_fine_grained = self.use_cache
options.local_partial_types = True
options.enable_incomplete_features = True
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
# Treat empty bodies safely for these test cases.
options.allow_empty_bodies = not testcase.name.endswith("_no_empty")
if re.search("flags:.*--follow-imports", source) is None:
Expand Down
4 changes: 2 additions & 2 deletions mypy/test/testsemanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource
from mypy.nodes import TypeInfo
from mypy.options import Options
from mypy.options import TYPE_VAR_TUPLE, UNPACK, Options
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import (
Expand Down Expand Up @@ -46,7 +46,7 @@ def get_semanal_options(program_text: str, testcase: DataDrivenTestCase) -> Opti
options.semantic_analysis_only = True
options.show_traceback = True
options.python_version = PYTHON3_VERSION
options.enable_incomplete_features = True
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
return options


Expand Down
3 changes: 2 additions & 1 deletion mypy/test/testtransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from mypy import build
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource
from mypy.options import TYPE_VAR_TUPLE, UNPACK
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal, normalize_error_messages, parse_options
Expand Down Expand Up @@ -39,7 +40,7 @@ def test_transform(testcase: DataDrivenTestCase) -> None:
options = parse_options(src, testcase, 1)
options.use_builtins_fixtures = True
options.semantic_analysis_only = True
options.enable_incomplete_features = True
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
options.show_traceback = True
result = build.build(
sources=[BuildSource("main", None, src)], options=options, alt_lib_path=test_temp_dir
Expand Down
6 changes: 2 additions & 4 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
check_arg_names,
get_nongen_builtins,
)
from mypy.options import Options
from mypy.options import UNPACK, Options
from mypy.plugin import AnalyzeTypeContext, Plugin, TypeAnalyzerPluginInterface
from mypy.semanal_shared import SemanticAnalyzerCoreInterface, paramspec_args, paramspec_kwargs
from mypy.tvar_scope import TypeVarLikeScope
Expand Down Expand Up @@ -569,9 +569,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
# In most contexts, TypeGuard[...] acts as an alias for bool (ignoring its args)
return self.named_type("builtins.bool")
elif fullname in ("typing.Unpack", "typing_extensions.Unpack"):
# We don't want people to try to use this yet.
if not self.options.enable_incomplete_features:
self.fail('"Unpack" is not supported yet, use --enable-incomplete-features', t)
if not self.api.incomplete_feature_enabled(UNPACK, t):
return AnyType(TypeOfAny.from_error)
return UnpackType(self.anal_type(t.args[0]), line=t.line, column=t.column)
return None
Expand Down
2 changes: 1 addition & 1 deletion mypyc/test-data/run-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1242,7 +1242,7 @@ def g() -> None:

g()

[case testIncompleteFeatureUnpackKwargsCompiled]
[case testUnpackKwargsCompiled]
from typing_extensions import Unpack, TypedDict

class Person(TypedDict):
Expand Down
5 changes: 2 additions & 3 deletions mypyc/test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from mypy import build
from mypy.errors import CompileError
from mypy.options import Options
from mypy.options import TYPE_VAR_TUPLE, UNPACK, Options
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase
from mypy.test.helpers import assert_module_equivalence, perform_file_operations
Expand Down Expand Up @@ -194,8 +194,7 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
options.preserve_asts = True
options.allow_empty_bodies = True
options.incremental = self.separate
if "IncompleteFeature" in testcase.name:
options.enable_incomplete_features = True
options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]

# Avoid checking modules/packages named 'unchecked', to provide a way
# to test interacting with code we don't have types for.
Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-flags.test
Original file line number Diff line number Diff line change
Expand Up @@ -2117,3 +2117,14 @@ x: int = "" # E: Incompatible types in assignment (expression has type "str", v
[case testHideErrorCodes]
# flags: --hide-error-codes
x: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int")

[case testTypeVarTupleDisabled_no_incomplete]
from typing_extensions import TypeVarTuple
Ts = TypeVarTuple("Ts") # E: "TypeVarTuple" support is experimental, use --enable-incomplete-feature=TypeVarTuple to enable
[builtins fixtures/tuple.pyi]

[case testTypeVarTupleEnabled_no_incomplete]
# flags: --enable-incomplete-feature=TypeVarTuple
from typing_extensions import TypeVarTuple
Ts = TypeVarTuple("Ts") # OK
[builtins fixtures/tuple.pyi]
4 changes: 3 additions & 1 deletion test-data/unit/cmdline.test
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,6 @@ exclude = (?x)(
[out]
c/cpkg.py:1: error: "int" not callable


[case testCmdlineTimingStats]
# cmd: mypy --timing-stats timing.txt .
[file b/__init__.py]
Expand All @@ -1435,6 +1434,9 @@ b\.c \d+
# cmd: mypy --enable-incomplete-features a.py
[file a.py]
pass
[out]
Warning: --enable-incomplete-features is deprecated, use --enable-incomplete-feature=FEATURE instead
== Return code: 0

[case testShadowTypingModuleEarlyLoad]
# cmd: mypy dir
Expand Down
1 change: 0 additions & 1 deletion test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -9901,7 +9901,6 @@ x = 0 # Arbitrary change to trigger reprocessing
a.py:3: note: Revealed type is "Tuple[Literal[1]?, Literal['x']?]"

[case testUnpackKwargsUpdateFine]
# flags: --enable-incomplete-features
import m
[file shared.py]
from typing_extensions import TypedDict
Expand Down
3 changes: 3 additions & 0 deletions test-data/unit/lib-stub/typing_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class _SpecialForm:
def __getitem__(self, typeargs: Any) -> Any:
pass

def __call__(self, arg: Any) -> Any:
pass

NamedTuple = 0
Protocol: _SpecialForm = ...
def runtime_checkable(x: _T) -> _T: pass
Expand Down