Skip to content

Commit 5a95f96

Browse files
authored
dmypy suggest: add a flag to try using unicode (#6996)
This allows preferring unicode in annotations generated for Python 2 code. pyannotate will spell unicode as Text, so this helps faciliate porting to Python 3.
1 parent 5fdb16b commit 5a95f96

File tree

4 files changed

+62
-24
lines changed

4 files changed

+62
-24
lines changed

mypy/dmypy/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def __init__(self, prog: str) -> None:
108108
help="Only produce suggestions that cause no errors")
109109
p.add_argument('--no-any', action='store_true',
110110
help="Only produce suggestions that don't contain Any")
111+
p.add_argument('--try-text', action='store_true',
112+
help="Try using unicode wherever str is inferred")
111113
p.add_argument('--callsites', action='store_true',
112114
help="Find callsites instead of suggesting a type")
113115

@@ -363,7 +365,7 @@ def do_suggest(args: argparse.Namespace) -> None:
363365
"""
364366
response = request(args.status_file, 'suggest', function=args.function,
365367
json=args.json, callsites=args.callsites, no_errors=args.no_errors,
366-
no_any=args.no_any)
368+
no_any=args.no_any, try_text=args.try_text)
367369
check_output(response, verbose=False, junit_xml=None, perf_stats_file=None)
368370

369371

mypy/suggestions.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828

2929
from mypy.state import strict_optional_set
3030
from mypy.types import (
31-
Type, AnyType, TypeOfAny, CallableType, UnionType, NoneType, Instance, TupleType, is_optional,
31+
Type, AnyType, TypeOfAny, CallableType, UnionType, NoneType, Instance, TupleType,
3232
TypeVarType, FunctionLike,
33-
TypeStrVisitor,
33+
TypeStrVisitor, TypeTranslator,
34+
is_optional,
3435
)
3536
from mypy.build import State, Graph
3637
from mypy.nodes import (
@@ -145,7 +146,8 @@ class SuggestionEngine:
145146
def __init__(self, fgmanager: FineGrainedBuildManager,
146147
json: bool,
147148
no_errors: bool = False,
148-
no_any: bool = False) -> None:
149+
no_any: bool = False,
150+
try_text: bool = False) -> None:
149151
self.fgmanager = fgmanager
150152
self.manager = fgmanager.manager
151153
self.plugin = self.manager.plugin
@@ -154,6 +156,7 @@ def __init__(self, fgmanager: FineGrainedBuildManager,
154156
self.give_json = json
155157
self.no_errors = no_errors
156158
self.no_any = no_any
159+
self.try_text = try_text
157160

158161
self.max_guesses = 16
159162

@@ -251,13 +254,20 @@ def get_default_arg_types(self, state: State, fdef: FuncDef) -> List[Optional[Ty
251254
return [self.manager.all_types[arg.initializer] if arg.initializer else None
252255
for arg in fdef.arguments]
253256

257+
def add_adjustments(self, typs: List[Type]) -> List[Type]:
258+
if not self.try_text or self.manager.options.python_version[0] != 2:
259+
return typs
260+
translator = StrToText(self.builtin_type)
261+
return dedup(typs + [tp.accept(translator) for tp in typs])
262+
254263
def get_guesses(self, is_method: bool, base: CallableType, defaults: List[Optional[Type]],
255264
callsites: List[Callsite]) -> List[CallableType]:
256265
"""Compute a list of guesses for a function's type.
257266
258267
This focuses just on the argument types, and doesn't change the provided return type.
259268
"""
260269
options = self.get_args(is_method, base, defaults, callsites)
270+
options = [self.add_adjustments(tps) for tps in options]
261271
return [base.copy_modified(arg_types=list(x)) for x in itertools.product(*options)]
262272

263273
def get_callsites(self, func: FuncDef) -> Tuple[List[Callsite], List[str]]:
@@ -292,7 +302,7 @@ def find_best(self, func: FuncDef, guesses: List[CallableType]) -> Tuple[Callabl
292302
raise SuggestionFailure("No guesses that match criteria!")
293303
errors = {guess: self.try_type(func, guess) for guess in guesses}
294304
best = min(guesses,
295-
key=lambda s: (count_errors(errors[s]), score_callable(s)))
305+
key=lambda s: (count_errors(errors[s]), self.score_callable(s)))
296306
return best, count_errors(errors[best])
297307

298308
def get_suggestion(self, function: str) -> str:
@@ -501,6 +511,25 @@ def format_callable(self,
501511
def format_type(self, cur_module: Optional[str], typ: Type) -> str:
502512
return typ.accept(TypeFormatter(cur_module, self.graph))
503513

514+
def score_type(self, t: Type) -> int:
515+
"""Generate a score for a type that we use to pick which type to use.
516+
517+
Lower is better, prefer non-union/non-any types. Don't penalize optionals.
518+
"""
519+
if isinstance(t, AnyType):
520+
return 20
521+
if isinstance(t, UnionType):
522+
if any(isinstance(x, AnyType) for x in t.items):
523+
return 20
524+
if not is_optional(t):
525+
return 10
526+
if self.try_text and isinstance(t, Instance) and t.type.fullname() == 'builtins.str':
527+
return 1
528+
return 0
529+
530+
def score_callable(self, t: CallableType) -> int:
531+
return sum([self.score_type(x) for x in t.arg_types])
532+
504533

505534
class TypeFormatter(TypeStrVisitor):
506535
"""Visitor used to format types
@@ -555,6 +584,17 @@ def visit_tuple_type(self, t: TupleType) -> str:
555584
return 'Tuple[{}]'.format(s)
556585

557586

587+
class StrToText(TypeTranslator):
588+
def __init__(self, builtin_type: Callable[[str], Instance]) -> None:
589+
self.text_type = builtin_type('builtins.unicode')
590+
591+
def visit_instance(self, t: Instance) -> Type:
592+
if t.type.fullname() == 'builtins.str':
593+
return self.text_type
594+
else:
595+
return super().visit_instance(t)
596+
597+
558598
def generate_type_combinations(types: List[Type]) -> List[Type]:
559599
"""Generate possible combinations of a list of types.
560600
@@ -573,25 +613,6 @@ def count_errors(msgs: List[str]) -> int:
573613
return len([x for x in msgs if ' error: ' in x])
574614

575615

576-
def score_type(t: Type) -> int:
577-
"""Generate a score for a type that we use to pick which type to use.
578-
579-
Lower is better, prefer non-union/non-any types. Don't penalize optionals.
580-
"""
581-
if isinstance(t, AnyType):
582-
return 2
583-
if isinstance(t, UnionType):
584-
if any(isinstance(x, AnyType) for x in t.items):
585-
return 2
586-
if not is_optional(t):
587-
return 1
588-
return 0
589-
590-
591-
def score_callable(t: CallableType) -> int:
592-
return sum([score_type(x) for x in t.arg_types])
593-
594-
595616
def callable_has_any(t: CallableType) -> int:
596617
# We count a bare None in argument position as Any, since
597618
# pyannotate turns it into Optional[Any]

mypy/test/testfinegrained.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,11 @@ def maybe_suggest(self, step: int, server: Server, src: str) -> List[str]:
270270
callsites = '--callsites' in flags
271271
no_any = '--no-any' in flags
272272
no_errors = '--no-errors' in flags
273+
try_text = '--try-text' in flags
273274
res = cast(Dict[str, Any],
274275
server.cmd_suggest(
275276
target.strip(), json=json, no_any=no_any, no_errors=no_errors,
277+
try_text=try_text,
276278
callsites=callsites))
277279
val = res['error'] if 'error' in res else res['out'] + res['err']
278280
output.extend(val.strip().split('\n'))

test-data/unit/fine-grained-suggest.test

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ Foo('lol')
175175
(str) -> None
176176
==
177177

178+
[case testSuggestTryText]
179+
# flags: --py2
180+
# suggest: --try-text foo.foo
181+
[file foo.py]
182+
def foo(s):
183+
return s
184+
[file bar.py]
185+
from foo import foo
186+
foo('lol')
187+
[out]
188+
(unicode) -> unicode
189+
==
190+
178191
[case testSuggestInferMethod1]
179192
# flags: --strict-optional
180193
# suggest: --no-any foo.Foo.foo

0 commit comments

Comments
 (0)