Skip to content

Add daemon command to get type of an expression #13209

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

Merged
merged 45 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9091f19
Implement server side logic
Jul 19, 2022
cb9b0ac
More work; add test
Jul 20, 2022
1e8bde7
Updated error message
Jul 20, 2022
d4417e5
Add end positions to more things
Jul 20, 2022
2616d49
Fix typo
Jul 20, 2022
5cb1d35
Reshuffle vistor
Jul 20, 2022
53d72d4
Add docs
Jul 20, 2022
6567878
Fixes
Jul 20, 2022
3a5e21d
More fixes
Jul 20, 2022
8292b5b
Merge remote-tracking branch 'upstream/master' into get-type
Jul 20, 2022
2d6b6bd
Make current logic explicit and remove dirty hacks
Jul 21, 2022
b3fbc04
Add also a test for class
Jul 21, 2022
a82c412
Fix lint
Jul 21, 2022
662de87
Add comment
Jul 21, 2022
b69bcbe
Few more fixes
Jul 21, 2022
593deae
Support providing inexact location
Jul 21, 2022
4198bc5
Add useful flags
Jul 21, 2022
8752280
Update docs
Jul 21, 2022
6989eb8
Move logic to a separate file
Jul 21, 2022
4ba1ca0
Merge remote-tracking branch 'upstream/master' into get-type
Jul 21, 2022
5a6e32d
Start moving towards more extendable API
Jul 21, 2022
4ed2a26
More work towards better API
Jul 21, 2022
3feff99
Add basic support for attributes
Jul 21, 2022
4086b03
Some attrs format tweaks
Jul 21, 2022
f474291
Start working on go to definition
Jul 22, 2022
3a20662
More fixes
Jul 22, 2022
b387fb9
More tests
Jul 22, 2022
19a9307
Fix self-check
Jul 22, 2022
8973f9e
Add support for unions
Jul 22, 2022
f0afc22
Better support for definition
Jul 22, 2022
5d59545
Fix self-check
Jul 22, 2022
9bf8d64
Start adding unit tests
Jul 22, 2022
4a152f9
Fix lookup of locals
Jul 22, 2022
1a5ab92
More tests and corner cases
Jul 22, 2022
3545e50
Fix error message
Jul 22, 2022
96870bc
Fix typo
Jul 22, 2022
80360d8
Fix class attributes
Jul 22, 2022
95cb99d
Complete adding unit tests
Jul 22, 2022
eecd5b4
Merge commit 'c898b90358f28123c429717c6c8c0051806d7240' into get-type
Jul 27, 2022
76effc0
Aply black
Jul 27, 2022
c896be5
Apply isort
Jul 27, 2022
631e2cd
Merge remote-tracking branch 'upstream/master' into get-type
Jul 27, 2022
ef7b55a
Add line
Jul 27, 2022
931dbd1
Fix applying black
Jul 27, 2022
15f1d3c
Fix WS in docstring
Jul 27, 2022
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
131 changes: 129 additions & 2 deletions docs/source/mypy_daemon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ Additional daemon flags
Write performance profiling information to ``FILE``. This is only available
for the ``check``, ``recheck``, and ``run`` commands.

.. option:: --export-types

Store all expression types in memory for future use. This is useful to speed
up future calls to ``dmypy inspect`` (but uses more memory). Only valid for
``check``, ``recheck``, and ``run`` command.

Static inference of annotations
*******************************

Expand Down Expand Up @@ -243,8 +249,129 @@ command.

Set the maximum number of types to try for a function (default: ``64``).

.. TODO: Add similar sections about go to definition, find usages, and
reveal type when added, and then move this to a separate file.
Statically inspect expressions
******************************

The daemon allows to get declared or inferred type of an expression (or other
information about an expression, such as known attributes or definition location)
using ``dmypy inspect LOCATION`` command. The location of the expression should be
specified in the format ``path/to/file.py:line:column[:end_line:end_column]``.
Both line and column are 1-based. Both start and end position are inclusive.
These rules match how mypy prints the error location in error messages.

If a span is given (i.e. all 4 numbers), then only an exactly matching expression
is inspected. If only a position is given (i.e. 2 numbers, line and column), mypy
will inspect all *expressions*, that include this position, starting from the
innermost one.

Consider this Python code snippet:

.. code-block:: python

def foo(x: int, longer_name: str) -> None:
x
longer_name

Here to find the type of ``x`` one needs to call ``dmypy inspect src.py:2:5:2:5``
or ``dmypy inspect src.py:2:5``. While for ``longer_name`` one needs to call
``dmypy inspect src.py:3:5:3:15`` or, for example, ``dmypy inspect src.py:3:10``.
Please note that this command is only valid after daemon had a successful type
check (without parse errors), so that types are populated, e.g. using
``dmypy check``. In case where multiple expressions match the provided location,
their types are returned separated by a newline.

Important note: it is recommended to check files with :option:`--export-types`
since otherwise most inspections will not work without :option:`--force-reload`.

.. option:: --show INSPECTION

What kind of inspection to run for expression(s) found. Currently the supported
inspections are:

* ``type`` (default): Show the best known type of a given expression.
* ``attrs``: Show which attributes are valid for an expression (e.g. for
auto-completion). Format is ``{"Base1": ["name_1", "name_2", ...]; "Base2": ...}``.
Names are sorted by method resolution order. If expression refers to a module,
then module attributes will be under key like ``"<full.module.name>"``.
* ``definition`` (experimental): Show the definition location for a name
expression or member expression. Format is ``path/to/file.py:line:column:Symbol``.
If multiple definitions are found (e.g. for a Union attribute), they are
separated by comma.

.. option:: --verbose

Increase verbosity of types string representation (can be repeated).
For example, this will print fully qualified names of instance types (like
``"builtins.str"``), instead of just a short name (like ``"str"``).

.. option:: --limit NUM

If the location is given as ``line:column``, this will cause daemon to
return only at most ``NUM`` inspections of innermost expressions.
Value of 0 means no limit (this is the default). For example, if one calls
``dmypy inspect src.py:4:10 --limit=1`` with this code

.. code-block:: python

def foo(x: int) -> str: ..
def bar(x: str) -> None: ...
baz: int
bar(foo(baz))

This will output just one type ``"int"`` (for ``baz`` name expression).
While without the limit option, it would output all three types: ``"int"``,
``"str"``, and ``"None"``.

.. option:: --include-span

With this option on, the daemon will prepend each inspection result with
the full span of corresponding expression, formatted as ``1:2:1:4 -> "int"``.
This may be useful in case multiple expressions match a location.

.. option:: --include-kind

With this option on, the daemon will prepend each inspection result with
the kind of corresponding expression, formatted as ``NameExpr -> "int"``.
If both this option and :option:`--include-span` are on, the kind will
appear first, for example ``NameExpr:1:2:1:4 -> "int"``.

.. option:: --include-object-attrs

This will make the daemon include attributes of ``object`` (excluded by
default) in case of an ``atts`` inspection.

.. option:: --union-attrs

Include attributes valid for some of possible expression types (by default
an intersection is returned). This is useful for union types of type variables
with values. For example, with this code:

.. code-block:: python

from typing import Union

class A:
x: int
z: int
class B:
y: int
z: int
var: Union[A, B]
var

The command ``dmypy inspect --show attrs src.py:10:1`` will return
``{"A": ["z"], "B": ["z"]}``, while with ``--union-attrs`` it will return
``{"A": ["x", "z"], "B": ["y", "z"]}``.

.. option:: --force-reload

Force re-parsing and re-type-checking file before inspection. By default
this is done only when needed (for example file was not loaded from cache
or daemon was initially run without ``--export-types`` mypy option),
since reloading may be slow (up to few seconds for very large files).

.. TODO: Add similar section about find usages when added, and then move
this to a separate file.


.. _watchman: https://facebook.github.io/watchman/
Expand Down
10 changes: 5 additions & 5 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1727,7 +1727,7 @@ def expand_typevars(
# Make a copy of the function to check for each combination of
# value restricted type variables. (Except when running mypyc,
# where we need one canonical version of the function.)
if subst and not self.options.mypyc:
if subst and not (self.options.mypyc or self.options.inspections):
result: List[Tuple[FuncItem, CallableType]] = []
for substitutions in itertools.product(*subst):
mapping = dict(substitutions)
Expand Down Expand Up @@ -3205,7 +3205,7 @@ def check_assignment_to_multiple_lvalues(
lr_pairs = list(zip(left_lvs, left_rvs))
if star_lv:
rv_list = ListExpr(star_rvs)
rv_list.set_line(rvalue.get_line())
rv_list.set_line(rvalue)
lr_pairs.append((star_lv.expr, rv_list))
lr_pairs.extend(zip(right_lvs, right_rvs))

Expand Down Expand Up @@ -3406,7 +3406,7 @@ def check_multi_assignment_from_tuple(
list_expr = ListExpr(
[self.temp_node(rv_type, context) for rv_type in star_rv_types]
)
list_expr.set_line(context.get_line())
list_expr.set_line(context)
self.check_assignment(star_lv.expr, list_expr, infer_lvalue_type)
for lv, rv_type in zip(right_lvs, right_rv_types):
self.check_assignment(lv, self.temp_node(rv_type, context), infer_lvalue_type)
Expand Down Expand Up @@ -4065,7 +4065,7 @@ def visit_if_stmt(self, s: IfStmt) -> None:
def visit_while_stmt(self, s: WhileStmt) -> None:
"""Type check a while statement."""
if_stmt = IfStmt([s.expr], [s.body], None)
if_stmt.set_line(s.get_line(), s.get_column())
if_stmt.set_line(s)
self.accept_loop(if_stmt, s.else_body, exit_condition=s.expr)

def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None:
Expand Down Expand Up @@ -6540,7 +6540,7 @@ def flatten_types(t: Type) -> List[Type]:

def expand_func(defn: FuncItem, map: Dict[TypeVarId, Type]) -> FuncItem:
visitor = TypeTransformVisitor(map)
ret = defn.accept(visitor)
ret = visitor.node(defn)
assert isinstance(ret, FuncItem)
return ret

Expand Down
4 changes: 4 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ def _analyze_member_access(
elif isinstance(typ, NoneType):
return analyze_none_member_access(name, typ, mx)
elif isinstance(typ, TypeVarLikeType):
if isinstance(typ, TypeVarType) and typ.values:
return _analyze_member_access(
name, make_simplified_union(typ.values), mx, override_info
)
return _analyze_member_access(name, typ.upper_bound, mx, override_info)
elif isinstance(typ, DeletedType):
mx.msg.deleted_as_rvalue(typ, mx.context)
Expand Down
124 changes: 119 additions & 5 deletions mypy/dmypy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ def __init__(self, prog: str) -> None:
p.add_argument("--junit-xml", help="Write junit.xml to the given file")
p.add_argument("--perf-stats-file", help="write performance information to the given file")
p.add_argument("files", metavar="FILE", nargs="+", help="File (or directory) to check")
p.add_argument(
"--export-types",
action="store_true",
help="Store types of all expressions in a shared location (useful for inspections)",
)

run_parser = p = subparsers.add_parser(
"run",
Expand All @@ -96,6 +101,11 @@ def __init__(self, prog: str) -> None:
"--timeout", metavar="TIMEOUT", type=int, help="Server shutdown timeout (in seconds)"
)
p.add_argument("--log-file", metavar="FILE", type=str, help="Direct daemon stdout/stderr to FILE")
p.add_argument(
"--export-types",
action="store_true",
help="Store types of all expressions in a shared location (useful for inspections)",
)
p.add_argument(
"flags",
metavar="ARG",
Expand All @@ -113,6 +123,11 @@ def __init__(self, prog: str) -> None:
p.add_argument("-q", "--quiet", action="store_true", help=argparse.SUPPRESS) # Deprecated
p.add_argument("--junit-xml", help="Write junit.xml to the given file")
p.add_argument("--perf-stats-file", help="write performance information to the given file")
p.add_argument(
"--export-types",
action="store_true",
help="Store types of all expressions in a shared location (useful for inspections)",
)
p.add_argument(
"--update",
metavar="FILE",
Expand Down Expand Up @@ -164,6 +179,68 @@ def __init__(self, prog: str) -> None:
help="Set the maximum number of types to try for a function (default 64)",
)

inspect_parser = p = subparsers.add_parser(
"inspect", help="Locate and statically inspect expression(s)"
)
p.add_argument(
"location",
metavar="LOCATION",
type=str,
help="Location specified as path/to/file.py:line:column[:end_line:end_column]."
" If position is given (i.e. only line and column), this will return all"
" enclosing expressions",
)
p.add_argument(
"--show",
metavar="INSPECTION",
type=str,
default="type",
choices=["type", "attrs", "definition"],
help="What kind of inspection to run",
)
p.add_argument(
"--verbose",
"-v",
action="count",
default=0,
help="Increase verbosity of the type string representation (can be repeated)",
)
p.add_argument(
"--limit",
metavar="NUM",
type=int,
default=0,
help="Return at most NUM innermost expressions (if position is given); 0 means no limit",
)
p.add_argument(
"--include-span",
action="store_true",
help="Prepend each inspection result with the span of corresponding expression"
' (e.g. 1:2:3:4:"int")',
)
p.add_argument(
"--include-kind",
action="store_true",
help="Prepend each inspection result with the kind of corresponding expression"
' (e.g. NameExpr:"int")',
)
p.add_argument(
"--include-object-attrs",
action="store_true",
help='Include attributes of "object" in "attrs" inspection',
)
p.add_argument(
"--union-attrs",
action="store_true",
help="Include attributes valid for some of possible expression types"
" (by default an intersection is returned)",
)
p.add_argument(
"--force-reload",
action="store_true",
help="Re-parse and re-type-check file before inspection (may be slow)",
)

hang_parser = p = subparsers.add_parser("hang", help="Hang for 100 seconds")

daemon_parser = p = subparsers.add_parser("daemon", help="Run daemon in foreground")
Expand Down Expand Up @@ -321,12 +398,24 @@ def do_run(args: argparse.Namespace) -> None:
# Bad or missing status file or dead process; good to start.
start_server(args, allow_sources=True)
t0 = time.time()
response = request(args.status_file, "run", version=__version__, args=args.flags)
response = request(
args.status_file,
"run",
version=__version__,
args=args.flags,
export_types=args.export_types,
)
# If the daemon signals that a restart is necessary, do it
if "restart" in response:
print(f"Restarting: {response['restart']}")
restart_server(args, allow_sources=True)
response = request(args.status_file, "run", version=__version__, args=args.flags)
response = request(
args.status_file,
"run",
version=__version__,
args=args.flags,
export_types=args.export_types,
)

t1 = time.time()
response["roundtrip_time"] = t1 - t0
Expand Down Expand Up @@ -383,7 +472,7 @@ def do_kill(args: argparse.Namespace) -> None:
def do_check(args: argparse.Namespace) -> None:
"""Ask the daemon to check a list of files."""
t0 = time.time()
response = request(args.status_file, "check", files=args.files)
response = request(args.status_file, "check", files=args.files, export_types=args.export_types)
t1 = time.time()
response["roundtrip_time"] = t1 - t0
check_output(response, args.verbose, args.junit_xml, args.perf_stats_file)
Expand All @@ -406,9 +495,15 @@ def do_recheck(args: argparse.Namespace) -> None:
"""
t0 = time.time()
if args.remove is not None or args.update is not None:
response = request(args.status_file, "recheck", remove=args.remove, update=args.update)
response = request(
args.status_file,
"recheck",
export_types=args.export_types,
remove=args.remove,
update=args.update,
)
else:
response = request(args.status_file, "recheck")
response = request(args.status_file, "recheck", export_types=args.export_types)
t1 = time.time()
response["roundtrip_time"] = t1 - t0
check_output(response, args.verbose, args.junit_xml, args.perf_stats_file)
Expand Down Expand Up @@ -437,6 +532,25 @@ def do_suggest(args: argparse.Namespace) -> None:
check_output(response, verbose=False, junit_xml=None, perf_stats_file=None)


@action(inspect_parser)
def do_inspect(args: argparse.Namespace) -> None:
"""Ask daemon to print the type of an expression."""
response = request(
args.status_file,
"inspect",
show=args.show,
location=args.location,
verbosity=args.verbose,
limit=args.limit,
include_span=args.include_span,
include_kind=args.include_kind,
include_object_attrs=args.include_object_attrs,
union_attrs=args.union_attrs,
force_reload=args.force_reload,
)
check_output(response, verbose=False, junit_xml=None, perf_stats_file=None)


def check_output(
response: Dict[str, Any],
verbose: bool,
Expand Down
Loading