Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 09dc4a0

Browse files
authored
Python inspector service (#1090)
Add python inspector, a JSON-RPC subprocess to make queries to python for info.
1 parent 3b14020 commit 09dc4a0

5 files changed

Lines changed: 585 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright(c) Microsoft Corporation
2+
// All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
5+
// this file except in compliance with the License. You may obtain a copy of the
6+
// License at http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
9+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
10+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
11+
// MERCHANTABILITY OR NON-INFRINGEMENT.
12+
//
13+
// See the Apache Version 2.0 License for specific language governing
14+
// permissions and limitations under the License.
15+
16+
using System.Collections.Generic;
17+
using System.Threading;
18+
using System.Threading.Tasks;
19+
20+
namespace Microsoft.Python.Analysis.Inspection {
21+
public interface IPythonInspector {
22+
Task<ModuleMemberNamesResponse> GetModuleMemberNamesAsync(string moduleName, CancellationToken cancellationToken = default);
23+
24+
Task<string> GetModuleVersionAsync(string moduleName, CancellationToken cancellationToken = default);
25+
}
26+
27+
public class ModuleMemberNamesResponse {
28+
public IReadOnlyList<string> Members;
29+
public IReadOnlyList<string> All;
30+
}
31+
}

src/Analysis/Ast/Impl/Microsoft.Python.Analysis.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
<Content Include="scrape_module.py">
2828
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2929
</Content>
30+
<Content Include="inspector.py">
31+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
32+
</Content>
3033
<Content Include="Typeshed\**\*">
3134
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3235
</Content>

src/Analysis/Ast/Impl/inspector.py

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
# Python Tools for Visual Studio
2+
# Copyright(c) Microsoft Corporation
3+
# All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the License); you may not use
6+
# this file except in compliance with the License. You may obtain a copy of the
7+
# License at http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
10+
# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
11+
# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
12+
# MERCHANTABILITY OR NON-INFRINGEMENT.
13+
#
14+
# See the Apache Version 2.0 License for specific language governing
15+
# permissions and limitations under the License.
16+
17+
# Supported Python versions: 2.7, 3.5+
18+
# https://devguide.python.org/#status-of-python-branches
19+
20+
from __future__ import print_function
21+
22+
import json
23+
import sys
24+
import time
25+
import inspect
26+
import importlib
27+
import os.path
28+
29+
# Uncomment to send stderr somewhere readable.
30+
# sys.stderr = open(os.path.join(os.path.expanduser("~"), "log.txt"), "a")
31+
32+
if sys.version_info >= (3,):
33+
stdout = sys.stdout.buffer
34+
stdin = sys.stdin.buffer
35+
else:
36+
stdout = sys.stdout
37+
stdin = sys.stdin
38+
39+
if sys.platform == "win32":
40+
import os, msvcrt
41+
42+
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
43+
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
44+
45+
46+
def read_line():
47+
line = b""
48+
while True:
49+
try:
50+
line += stdin.readline()
51+
except:
52+
raise EOFError
53+
if not line:
54+
raise EOFError
55+
if line.endswith(b"\r\n"):
56+
return line[:-2]
57+
58+
59+
def write_stdout(data):
60+
if not isinstance(data, bytes):
61+
data = data.encode("utf-8")
62+
63+
stdout.write(data)
64+
stdout.flush()
65+
66+
67+
METHOD_NOT_FOUND = -32601
68+
INTERNAL_ERROR = -32603
69+
70+
71+
def read_request():
72+
headers = dict()
73+
74+
while True:
75+
line = read_line()
76+
77+
if line == b"":
78+
break
79+
80+
key, _, value = line.partition(b": ")
81+
headers[key] = value
82+
83+
length = int(headers[b"Content-Length"])
84+
85+
body = b""
86+
while length > 0:
87+
chunk = stdin.read(length)
88+
body += chunk
89+
length -= len(chunk)
90+
91+
request = json.loads(body)
92+
return Request(request)
93+
94+
95+
def requests():
96+
while True:
97+
try:
98+
request = read_request()
99+
except EOFError:
100+
return
101+
102+
yield request
103+
104+
105+
def write_response(id, d):
106+
response = {"jsonrpc": "2.0", "id": id}
107+
response.update(d)
108+
109+
s = json.dumps(response)
110+
111+
data = "Content-Length: {}\r\n\r\n".format(len(s)) + s
112+
write_stdout(data)
113+
114+
115+
class Request(object):
116+
def __init__(self, request):
117+
self.id = request["id"]
118+
self.method = request["method"]
119+
self.params = request.get("params", None)
120+
121+
def write_result(self, result):
122+
write_response(self.id, {"result": result})
123+
124+
def write_error(self, code, message):
125+
write_response(self.id, {"error": {"code": code, "message": message}})
126+
127+
128+
class Mux(object):
129+
def __init__(self):
130+
self.handlers = {}
131+
132+
def handler(self, method):
133+
def decorator(func):
134+
self.handlers[method] = func
135+
return func
136+
137+
return decorator
138+
139+
def handle(self, request):
140+
handler = self.handlers.get(request.method, None)
141+
142+
if not handler:
143+
request.write_error(
144+
METHOD_NOT_FOUND, "method {} not found".format(request.method)
145+
)
146+
return
147+
148+
try:
149+
result = handler(*request.params)
150+
except Exception as e:
151+
request.write_error(INTERNAL_ERROR, str(e))
152+
else:
153+
request.write_result(result)
154+
155+
156+
mux = Mux()
157+
158+
159+
def do_not_inspect(v):
160+
# https://github.com/Microsoft/python-language-server/issues/740
161+
# https://github.com/cython/cython/issues/1470
162+
if type(v).__name__ != "fused_cython_function":
163+
return False
164+
165+
# If a fused function has __defaults__, then attempting to access
166+
# __kwdefaults__ will fail if generated before cython 0.29.6.
167+
return bool(getattr(v, "__defaults__", False))
168+
169+
170+
KNOWN_DIST_PREFIXES = {"PyQt5": ["PyQt5"], "PyQt5-sip": ["PyQt5.sip"]}
171+
172+
173+
def build_dist_prefixes():
174+
import pkg_resources
175+
176+
prefixes = dict()
177+
178+
# This iterates in the order things were added; no need to reverse.
179+
for d in pkg_resources.WorkingSet():
180+
top_level = None
181+
182+
try:
183+
top_level = d.get_metadata("top_level.txt")
184+
except:
185+
pass
186+
187+
module_prefixes = None
188+
if top_level:
189+
module_prefixes = top_level.splitlines()
190+
elif d.project_name:
191+
module_prefixes = KNOWN_DIST_PREFIXES.get(d.project_name, None)
192+
193+
if not module_prefixes:
194+
continue
195+
196+
for prefix in module_prefixes:
197+
prefix = prefix.strip()
198+
if prefix:
199+
prefixes[prefix] = d
200+
201+
return prefixes
202+
203+
204+
DIST_PREFIXES = None
205+
206+
207+
def find_dist(module_name):
208+
global DIST_PREFIXES
209+
if DIST_PREFIXES is None:
210+
DIST_PREFIXES = build_dist_prefixes()
211+
212+
while True:
213+
if not module_name:
214+
return None
215+
216+
d = DIST_PREFIXES.get(module_name, None)
217+
if d:
218+
return d
219+
220+
split = module_name.split(".")
221+
if len(split) < 2:
222+
return None
223+
224+
module_name = ".".join(split[:-1])
225+
226+
227+
@mux.handler("$/cancelRequest")
228+
def cancel_request(params):
229+
return None
230+
231+
232+
@mux.handler("moduleMemberNames")
233+
def module_member_names(module_name):
234+
try:
235+
module = importlib.import_module(module_name)
236+
except:
237+
return None
238+
239+
members = inspect.getmembers(module)
240+
241+
return {
242+
"members": [name for name, _ in members],
243+
"all": getattr(module, "__all__", None),
244+
}
245+
246+
247+
@mux.handler("moduleVersion")
248+
def module_version(module_name):
249+
version = None
250+
try:
251+
# TODO: iterate as in find_dist and import looking for __version__?
252+
module = importlib.import_module(module_name)
253+
version = getattr(module, "__version__", None)
254+
except:
255+
pass
256+
257+
if not version:
258+
try:
259+
d = find_dist(module_name)
260+
if d:
261+
version = d.version
262+
except:
263+
pass
264+
265+
return version
266+
267+
268+
def main():
269+
for request in requests():
270+
mux.handle(request)
271+
272+
273+
if __name__ == "__main__":
274+
main()

0 commit comments

Comments
 (0)