diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 504d62b05347..821905345add 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -172,6 +172,8 @@ overridden by the pattern sections matching the module name. - ``strict_boolean`` (Boolean, default False) makes using non-boolean expressions in conditions an error. +- ``no_implicit_optional`` (Boolean, default false) changes the treatment of + arguments with a default value of None by not implicitly making their type Optional Example ******* diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 19619cf58c6b..c5249a8c9588 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -28,6 +28,7 @@ from mypy import experiments from mypy import messages from mypy.errors import Errors +from mypy.options import Options try: from typed_ast import ast3 @@ -58,14 +59,12 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None, - pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION, - custom_typing_module: str = None) -> MypyFile: + options: Options = Options()) -> MypyFile: + """Parse a source file, without doing any semantic analysis. Return the parse tree. If errors is not provided, raise ParseError on failure. Otherwise, use the errors object to report parse errors. - - The pyversion (major, minor) argument determines the Python syntax variant. """ raise_on_error = False if errors is None: @@ -74,14 +73,16 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None, errors.set_file('' if fnam is None else fnam, None) is_stub_file = bool(fnam) and fnam.endswith('.pyi') try: - assert pyversion[0] >= 3 or is_stub_file - feature_version = pyversion[1] if not is_stub_file else defaults.PYTHON3_VERSION[1] + if is_stub_file: + feature_version = defaults.PYTHON3_VERSION[1] + else: + assert options.python_version[0] >= 3 + feature_version = options.python_version[1] ast = ast3.parse(source, fnam, 'exec', feature_version=feature_version) - tree = ASTConverter(pyversion=pyversion, + tree = ASTConverter(options=options, is_stub=is_stub_file, errors=errors, - custom_typing_module=custom_typing_module, ).visit(ast) tree.path = fnam tree.is_stub = is_stub_file @@ -136,17 +137,15 @@ def is_no_type_check_decorator(expr: ast3.expr) -> bool: class ASTConverter(ast3.NodeTransformer): # type: ignore # typeshed PR #931 def __init__(self, - pyversion: Tuple[int, int], + options: Options, is_stub: bool, - errors: Errors, - custom_typing_module: str = None) -> None: + errors: Errors) -> None: self.class_nesting = 0 self.imports = [] # type: List[ImportBase] - self.pyversion = pyversion + self.options = options self.is_stub = is_stub self.errors = errors - self.custom_typing_module = custom_typing_module def fail(self, msg: str, line: int, column: int) -> None: self.errors.report(line, column, msg) @@ -260,9 +259,9 @@ def translate_module_id(self, id: str) -> str: For example, translate '__builtin__' in Python 2 to 'builtins'. """ - if id == self.custom_typing_module: + if id == self.options.custom_typing_module: return 'typing' - elif id == '__builtin__' and self.pyversion[0] == 2: + elif id == '__builtin__' and self.options.python_version[0] == 2: # HACK: __builtin__ in Python 2 is aliases to builtins. However, the implementation # is named __builtin__.py (there is another layer of translation elsewhere). return 'builtins' @@ -388,7 +387,7 @@ def do_func_def(self, n: Union[ast3.FunctionDef, ast3.AsyncFunctionDef], return func_def def set_type_optional(self, type: Type, initializer: Expression) -> None: - if not experiments.STRICT_OPTIONAL: + if self.options.no_implicit_optional or not experiments.STRICT_OPTIONAL: return # Indicate that type should be wrapped in an Optional if arg is initialized to None. optional = isinstance(initializer, NameExpr) and initializer.name == 'None' @@ -855,16 +854,13 @@ def visit_Num(self, n: ast3.Num) -> Union[IntExpr, FloatExpr, ComplexExpr]: # Str(string s) @with_line def visit_Str(self, n: ast3.Str) -> Union[UnicodeExpr, StrExpr]: - if self.pyversion[0] >= 3 or self.is_stub: - # Hack: assume all string literals in Python 2 stubs are normal - # strs (i.e. not unicode). All stubs are parsed with the Python 3 - # parser, which causes unprefixed string literals to be interpreted - # as unicode instead of bytes. This hack is generally okay, - # because mypy considers str literals to be compatible with - # unicode. - return StrExpr(n.s) - else: - return UnicodeExpr(n.s) + # Hack: assume all string literals in Python 2 stubs are normal + # strs (i.e. not unicode). All stubs are parsed with the Python 3 + # parser, which causes unprefixed string literals to be interpreted + # as unicode instead of bytes. This hack is generally okay, + # because mypy considers str literals to be compatible with + # unicode. + return StrExpr(n.s) # Only available with typed_ast >= 0.6.2 if hasattr(ast3, 'JoinedStr'): @@ -894,11 +890,7 @@ def visit_Bytes(self, n: ast3.Bytes) -> Union[BytesExpr, StrExpr]: # The following line is a bit hacky, but is the best way to maintain # compatibility with how mypy currently parses the contents of bytes literals. contents = str(n.s)[2:-1] - - if self.pyversion[0] >= 3: - return BytesExpr(contents) - else: - return StrExpr(contents) + return BytesExpr(contents) # NameConstant(singleton value) def visit_NameConstant(self, n: ast3.NameConstant) -> NameExpr: diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index aca04187e57c..ef6c6d00c4fa 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -38,11 +38,11 @@ from mypy.types import ( Type, CallableType, AnyType, UnboundType, EllipsisType ) -from mypy import defaults from mypy import experiments from mypy import messages from mypy.errors import Errors from mypy.fastparse import TypeConverter, parse_type_comment +from mypy.options import Options try: from typed_ast import ast27 @@ -74,14 +74,11 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None, - pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION, - custom_typing_module: str = None) -> MypyFile: + options: Options = Options()) -> MypyFile: """Parse a source file, without doing any semantic analysis. Return the parse tree. If errors is not provided, raise ParseError on failure. Otherwise, use the errors object to report parse errors. - - The pyversion (major, minor) argument determines the Python syntax variant. """ raise_on_error = False if errors is None: @@ -90,12 +87,11 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None, errors.set_file('' if fnam is None else fnam, None) is_stub_file = bool(fnam) and fnam.endswith('.pyi') try: - assert pyversion[0] < 3 and not is_stub_file + assert options.python_version[0] < 3 and not is_stub_file ast = ast27.parse(source, fnam, 'exec') - tree = ASTConverter(pyversion=pyversion, + tree = ASTConverter(options=options, is_stub=is_stub_file, errors=errors, - custom_typing_module=custom_typing_module, ).visit(ast) assert isinstance(tree, MypyFile) tree.path = fnam @@ -137,17 +133,15 @@ def is_no_type_check_decorator(expr: ast27.expr) -> bool: class ASTConverter(ast27.NodeTransformer): def __init__(self, - pyversion: Tuple[int, int], + options: Options, is_stub: bool, - errors: Errors, - custom_typing_module: str = None) -> None: + errors: Errors) -> None: self.class_nesting = 0 self.imports = [] # type: List[ImportBase] - self.pyversion = pyversion + self.options = options self.is_stub = is_stub self.errors = errors - self.custom_typing_module = custom_typing_module def fail(self, msg: str, line: int, column: int) -> None: self.errors.report(line, column, msg) @@ -262,9 +256,9 @@ def translate_module_id(self, id: str) -> str: For example, translate '__builtin__' in Python 2 to 'builtins'. """ - if id == self.custom_typing_module: + if id == self.options.custom_typing_module: return 'typing' - elif id == '__builtin__' and self.pyversion[0] == 2: + elif id == '__builtin__': # HACK: __builtin__ in Python 2 is aliases to builtins. However, the implementation # is named __builtin__.py (there is another layer of translation elsewhere). return 'builtins' @@ -370,7 +364,7 @@ def visit_FunctionDef(self, n: ast27.FunctionDef) -> Statement: return func_def def set_type_optional(self, type: Type, initializer: Expression) -> None: - if not experiments.STRICT_OPTIONAL: + if self.options.no_implicit_optional or not experiments.STRICT_OPTIONAL: return # Indicate that type should be wrapped in an Optional if arg is initialized to None. optional = isinstance(initializer, NameExpr) and initializer.name == 'None' @@ -872,16 +866,9 @@ def visit_Str(self, s: ast27.Str) -> Expression: # The following line is a bit hacky, but is the best way to maintain # compatibility with how mypy currently parses the contents of bytes literals. contents = str(n)[2:-1] - - if self.pyversion[0] >= 3: - return BytesExpr(contents) - else: - return StrExpr(contents) + return StrExpr(contents) else: - if self.pyversion[0] >= 3 or self.is_stub: - return StrExpr(s.s) - else: - return UnicodeExpr(s.s) + return UnicodeExpr(s.s) # Ellipsis def visit_Ellipsis(self, n: ast27.Ellipsis) -> EllipsisExpr: diff --git a/mypy/main.py b/mypy/main.py index b90d82a30979..79690b08d100 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -236,6 +236,8 @@ def add_invertible_flag(flag: str, add_invertible_flag('--show-error-context', default=False, dest='show_error_context', help='Precede errors with "note:" messages explaining context') + add_invertible_flag('--no-implicit-optional', default=False, strict_flag=True, + help="don't assume arguments with default values of None are Optional") parser.add_argument('-i', '--incremental', action='store_true', help="enable module cache") parser.add_argument('--quick-and-dirty', action='store_true', diff --git a/mypy/options.py b/mypy/options.py index 8c8764200800..5e841bee0c6e 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -29,6 +29,7 @@ class Options: "warn_return_any", "ignore_errors", "strict_boolean", + "no_implicit_optional", } OPTIONS_AFFECTING_CACHE = PER_MODULE_OPTIONS | {"strict_optional", "quick_and_dirty"} @@ -92,6 +93,9 @@ def __init__(self) -> None: # Alternate way to show/hide strict-None-checking related errors self.show_none_errors = True + # Don't assume arguments with default values of None are Optional + self.no_implicit_optional = False + # Use script name instead of __main__ self.scripts_are_modules = False diff --git a/mypy/parse.py b/mypy/parse.py index ddcd226b9ff2..72e5ab468fed 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -22,12 +22,10 @@ def parse(source: Union[str, bytes], return mypy.fastparse.parse(source, fnam=fnam, errors=errors, - pyversion=options.python_version, - custom_typing_module=options.custom_typing_module) + options=options) else: import mypy.fastparse2 return mypy.fastparse2.parse(source, fnam=fnam, errors=errors, - pyversion=options.python_version, - custom_typing_module=options.custom_typing_module) + options=options) diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index e4fb2a16025c..b0d8769384c0 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -125,11 +125,10 @@ def f(x: int = None) -> None: f(None) [out] -[case testInferOptionalFromDefaultNoneWithFastParser] - -def f(x: int = None) -> None: - x + 1 # E: Unsupported left operand type for + (some union) -f(None) +[case testNoInferOptionalFromDefaultNone] +# flags: --no-implicit-optional +def f(x: int = None) -> None: # E: Incompatible types in assignment (expression has type None, variable has type "int") + pass [out] [case testInferOptionalFromDefaultNoneComment] @@ -139,12 +138,11 @@ def f(x=None): f(None) [out] -[case testInferOptionalFromDefaultNoneCommentWithFastParser] - -def f(x=None): +[case testNoInferOptionalFromDefaultNoneComment] +# flags: --no-implicit-optional +def f(x=None): # E: Incompatible types in assignment (expression has type None, variable has type "int") # type: (int) -> None - x + 1 # E: Unsupported left operand type for + (some union) -f(None) + pass [out] [case testInferOptionalType]