Skip to content

Commit 443c479

Browse files
ddfishergvanrossum
authored andcommitted
Add flag to avoid interpreting arguments with a default of None as Optional (#3248)
See python/typing#275.
1 parent 53879ef commit 443c479

File tree

7 files changed

+53
-70
lines changed

7 files changed

+53
-70
lines changed

docs/source/config_file.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ overridden by the pattern sections matching the module name.
178178
- ``strict_boolean`` (Boolean, default False) makes using non-boolean
179179
expressions in conditions an error.
180180

181+
- ``no_implicit_optional`` (Boolean, default false) changes the treatment of
182+
arguments with a default value of None by not implicitly making their type Optional
181183

182184
Example
183185
*******

mypy/fastparse.py

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from mypy import experiments
3131
from mypy import messages
3232
from mypy.errors import Errors
33+
from mypy.options import Options
3334

3435
try:
3536
from typed_ast import ast3
@@ -60,14 +61,12 @@
6061

6162

6263
def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None,
63-
pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION,
64-
custom_typing_module: str = None) -> MypyFile:
64+
options: Options = Options()) -> MypyFile:
65+
6566
"""Parse a source file, without doing any semantic analysis.
6667
6768
Return the parse tree. If errors is not provided, raise ParseError
6869
on failure. Otherwise, use the errors object to report parse errors.
69-
70-
The pyversion (major, minor) argument determines the Python syntax variant.
7170
"""
7271
raise_on_error = False
7372
if errors is None:
@@ -76,14 +75,16 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None,
7675
errors.set_file('<input>' if fnam is None else fnam, None)
7776
is_stub_file = bool(fnam) and fnam.endswith('.pyi')
7877
try:
79-
assert pyversion[0] >= 3 or is_stub_file
80-
feature_version = pyversion[1] if not is_stub_file else defaults.PYTHON3_VERSION[1]
78+
if is_stub_file:
79+
feature_version = defaults.PYTHON3_VERSION[1]
80+
else:
81+
assert options.python_version[0] >= 3
82+
feature_version = options.python_version[1]
8183
ast = ast3.parse(source, fnam, 'exec', feature_version=feature_version)
8284

83-
tree = ASTConverter(pyversion=pyversion,
85+
tree = ASTConverter(options=options,
8486
is_stub=is_stub_file,
8587
errors=errors,
86-
custom_typing_module=custom_typing_module,
8788
).visit(ast)
8889
tree.path = fnam
8990
tree.is_stub = is_stub_file
@@ -138,17 +139,15 @@ def is_no_type_check_decorator(expr: ast3.expr) -> bool:
138139

139140
class ASTConverter(ast3.NodeTransformer): # type: ignore # typeshed PR #931
140141
def __init__(self,
141-
pyversion: Tuple[int, int],
142+
options: Options,
142143
is_stub: bool,
143-
errors: Errors,
144-
custom_typing_module: str = None) -> None:
144+
errors: Errors) -> None:
145145
self.class_nesting = 0
146146
self.imports = [] # type: List[ImportBase]
147147

148-
self.pyversion = pyversion
148+
self.options = options
149149
self.is_stub = is_stub
150150
self.errors = errors
151-
self.custom_typing_module = custom_typing_module
152151

153152
def fail(self, msg: str, line: int, column: int) -> None:
154153
self.errors.report(line, column, msg)
@@ -262,9 +261,9 @@ def translate_module_id(self, id: str) -> str:
262261
263262
For example, translate '__builtin__' in Python 2 to 'builtins'.
264263
"""
265-
if id == self.custom_typing_module:
264+
if id == self.options.custom_typing_module:
266265
return 'typing'
267-
elif id == '__builtin__' and self.pyversion[0] == 2:
266+
elif id == '__builtin__' and self.options.python_version[0] == 2:
268267
# HACK: __builtin__ in Python 2 is aliases to builtins. However, the implementation
269268
# is named __builtin__.py (there is another layer of translation elsewhere).
270269
return 'builtins'
@@ -391,7 +390,7 @@ def do_func_def(self, n: Union[ast3.FunctionDef, ast3.AsyncFunctionDef],
391390
return func_def
392391

393392
def set_type_optional(self, type: Type, initializer: Expression) -> None:
394-
if not experiments.STRICT_OPTIONAL:
393+
if self.options.no_implicit_optional or not experiments.STRICT_OPTIONAL:
395394
return
396395
# Indicate that type should be wrapped in an Optional if arg is initialized to None.
397396
optional = isinstance(initializer, NameExpr) and initializer.name == 'None'
@@ -846,16 +845,13 @@ def visit_Num(self, n: ast3.Num) -> Union[IntExpr, FloatExpr, ComplexExpr]:
846845
# Str(string s)
847846
@with_line
848847
def visit_Str(self, n: ast3.Str) -> Union[UnicodeExpr, StrExpr]:
849-
if self.pyversion[0] >= 3 or self.is_stub:
850-
# Hack: assume all string literals in Python 2 stubs are normal
851-
# strs (i.e. not unicode). All stubs are parsed with the Python 3
852-
# parser, which causes unprefixed string literals to be interpreted
853-
# as unicode instead of bytes. This hack is generally okay,
854-
# because mypy considers str literals to be compatible with
855-
# unicode.
856-
return StrExpr(n.s)
857-
else:
858-
return UnicodeExpr(n.s)
848+
# Hack: assume all string literals in Python 2 stubs are normal
849+
# strs (i.e. not unicode). All stubs are parsed with the Python 3
850+
# parser, which causes unprefixed string literals to be interpreted
851+
# as unicode instead of bytes. This hack is generally okay,
852+
# because mypy considers str literals to be compatible with
853+
# unicode.
854+
return StrExpr(n.s)
859855

860856
# Only available with typed_ast >= 0.6.2
861857
if hasattr(ast3, 'JoinedStr'):
@@ -885,11 +881,7 @@ def visit_Bytes(self, n: ast3.Bytes) -> Union[BytesExpr, StrExpr]:
885881
# The following line is a bit hacky, but is the best way to maintain
886882
# compatibility with how mypy currently parses the contents of bytes literals.
887883
contents = str(n.s)[2:-1]
888-
889-
if self.pyversion[0] >= 3:
890-
return BytesExpr(contents)
891-
else:
892-
return StrExpr(contents)
884+
return BytesExpr(contents)
893885

894886
# NameConstant(singleton value)
895887
def visit_NameConstant(self, n: ast3.NameConstant) -> NameExpr:

mypy/fastparse2.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
from mypy.types import (
3939
Type, CallableType, AnyType, UnboundType, EllipsisType
4040
)
41-
from mypy import defaults
4241
from mypy import experiments
4342
from mypy import messages
4443
from mypy.errors import Errors
4544
from mypy.fastparse import TypeConverter, parse_type_comment
45+
from mypy.options import Options
4646

4747
try:
4848
from typed_ast import ast27
@@ -74,14 +74,11 @@
7474

7575

7676
def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None,
77-
pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION,
78-
custom_typing_module: str = None) -> MypyFile:
77+
options: Options = Options()) -> MypyFile:
7978
"""Parse a source file, without doing any semantic analysis.
8079
8180
Return the parse tree. If errors is not provided, raise ParseError
8281
on failure. Otherwise, use the errors object to report parse errors.
83-
84-
The pyversion (major, minor) argument determines the Python syntax variant.
8582
"""
8683
raise_on_error = False
8784
if errors is None:
@@ -90,12 +87,11 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None,
9087
errors.set_file('<input>' if fnam is None else fnam, None)
9188
is_stub_file = bool(fnam) and fnam.endswith('.pyi')
9289
try:
93-
assert pyversion[0] < 3 and not is_stub_file
90+
assert options.python_version[0] < 3 and not is_stub_file
9491
ast = ast27.parse(source, fnam, 'exec')
95-
tree = ASTConverter(pyversion=pyversion,
92+
tree = ASTConverter(options=options,
9693
is_stub=is_stub_file,
9794
errors=errors,
98-
custom_typing_module=custom_typing_module,
9995
).visit(ast)
10096
assert isinstance(tree, MypyFile)
10197
tree.path = fnam
@@ -137,17 +133,15 @@ def is_no_type_check_decorator(expr: ast27.expr) -> bool:
137133

138134
class ASTConverter(ast27.NodeTransformer):
139135
def __init__(self,
140-
pyversion: Tuple[int, int],
136+
options: Options,
141137
is_stub: bool,
142-
errors: Errors,
143-
custom_typing_module: str = None) -> None:
138+
errors: Errors) -> None:
144139
self.class_nesting = 0
145140
self.imports = [] # type: List[ImportBase]
146141

147-
self.pyversion = pyversion
142+
self.options = options
148143
self.is_stub = is_stub
149144
self.errors = errors
150-
self.custom_typing_module = custom_typing_module
151145

152146
def fail(self, msg: str, line: int, column: int) -> None:
153147
self.errors.report(line, column, msg)
@@ -262,9 +256,9 @@ def translate_module_id(self, id: str) -> str:
262256
263257
For example, translate '__builtin__' in Python 2 to 'builtins'.
264258
"""
265-
if id == self.custom_typing_module:
259+
if id == self.options.custom_typing_module:
266260
return 'typing'
267-
elif id == '__builtin__' and self.pyversion[0] == 2:
261+
elif id == '__builtin__':
268262
# HACK: __builtin__ in Python 2 is aliases to builtins. However, the implementation
269263
# is named __builtin__.py (there is another layer of translation elsewhere).
270264
return 'builtins'
@@ -370,7 +364,7 @@ def visit_FunctionDef(self, n: ast27.FunctionDef) -> Statement:
370364
return func_def
371365

372366
def set_type_optional(self, type: Type, initializer: Expression) -> None:
373-
if not experiments.STRICT_OPTIONAL:
367+
if self.options.no_implicit_optional or not experiments.STRICT_OPTIONAL:
374368
return
375369
# Indicate that type should be wrapped in an Optional if arg is initialized to None.
376370
optional = isinstance(initializer, NameExpr) and initializer.name == 'None'
@@ -870,16 +864,9 @@ def visit_Str(self, s: ast27.Str) -> Expression:
870864
# The following line is a bit hacky, but is the best way to maintain
871865
# compatibility with how mypy currently parses the contents of bytes literals.
872866
contents = str(n)[2:-1]
873-
874-
if self.pyversion[0] >= 3:
875-
return BytesExpr(contents)
876-
else:
877-
return StrExpr(contents)
867+
return StrExpr(contents)
878868
else:
879-
if self.pyversion[0] >= 3 or self.is_stub:
880-
return StrExpr(s.s)
881-
else:
882-
return UnicodeExpr(s.s)
869+
return UnicodeExpr(s.s)
883870

884871
# Ellipsis
885872
def visit_Ellipsis(self, n: ast27.Ellipsis) -> EllipsisExpr:

mypy/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ def add_invertible_flag(flag: str,
247247
add_invertible_flag('--show-error-context', default=False,
248248
dest='show_error_context',
249249
help='Precede errors with "note:" messages explaining context')
250+
add_invertible_flag('--no-implicit-optional', default=False, strict_flag=True,
251+
help="don't assume arguments with default values of None are Optional")
250252
parser.add_argument('-i', '--incremental', action='store_true',
251253
help="enable module cache")
252254
parser.add_argument('--quick-and-dirty', action='store_true',

mypy/options.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Options:
2929
"warn_return_any",
3030
"ignore_errors",
3131
"strict_boolean",
32+
"no_implicit_optional",
3233
}
3334

3435
OPTIONS_AFFECTING_CACHE = PER_MODULE_OPTIONS | {"strict_optional", "quick_and_dirty"}
@@ -92,6 +93,9 @@ def __init__(self) -> None:
9293
# Alternate way to show/hide strict-None-checking related errors
9394
self.show_none_errors = True
9495

96+
# Don't assume arguments with default values of None are Optional
97+
self.no_implicit_optional = False
98+
9599
# Use script name instead of __main__
96100
self.scripts_are_modules = False
97101

mypy/parse.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ def parse(source: Union[str, bytes],
2222
return mypy.fastparse.parse(source,
2323
fnam=fnam,
2424
errors=errors,
25-
pyversion=options.python_version,
26-
custom_typing_module=options.custom_typing_module)
25+
options=options)
2726
else:
2827
import mypy.fastparse2
2928
return mypy.fastparse2.parse(source,
3029
fnam=fnam,
3130
errors=errors,
32-
pyversion=options.python_version,
33-
custom_typing_module=options.custom_typing_module)
31+
options=options)

test-data/unit/check-optional.test

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,10 @@ def f(x: int = None) -> None:
125125
f(None)
126126
[out]
127127

128-
[case testInferOptionalFromDefaultNoneWithFastParser]
129-
130-
def f(x: int = None) -> None:
131-
x + 1 # E: Unsupported left operand type for + (some union)
132-
f(None)
128+
[case testNoInferOptionalFromDefaultNone]
129+
# flags: --no-implicit-optional
130+
def f(x: int = None) -> None: # E: Incompatible types in assignment (expression has type None, variable has type "int")
131+
pass
133132
[out]
134133

135134
[case testInferOptionalFromDefaultNoneComment]
@@ -139,12 +138,11 @@ def f(x=None):
139138
f(None)
140139
[out]
141140

142-
[case testInferOptionalFromDefaultNoneCommentWithFastParser]
143-
144-
def f(x=None):
141+
[case testNoInferOptionalFromDefaultNoneComment]
142+
# flags: --no-implicit-optional
143+
def f(x=None): # E: Incompatible types in assignment (expression has type None, variable has type "int")
145144
# type: (int) -> None
146-
x + 1 # E: Unsupported left operand type for + (some union)
147-
f(None)
145+
pass
148146
[out]
149147

150148
[case testInferOptionalType]

0 commit comments

Comments
 (0)