Skip to content
This repository was archived by the owner on Mar 8, 2020. It is now read-only.

Commit d2adf31

Browse files
authored
Merge pull request #142 from juanjux/compat
V1 compatibility layer
2 parents 375fd7e + 120047a commit d2adf31

9 files changed

Lines changed: 1027 additions & 139 deletions

File tree

bblfsh/compat.py

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
"""
2+
This file provides a compatibility layer with the old UAST V1 (or client-python
3+
v2) API. You can see a summary of that API here:
4+
5+
https://github.com/bblfsh/client-python/blob/d485273f457a174b40b820ad71195a739db04197/README.md
6+
7+
Note that this won't translate the XPath queries from the old projection to the new use;
8+
even when using this module you're expected to use expressions matching the new
9+
projection.
10+
11+
Note that since this is a pure Python translation layer, some performance
12+
impact is to be expected.
13+
"""
14+
import os
15+
import sys
16+
from typing import Union, List, Any, Optional
17+
18+
import grpc
19+
20+
import bblfsh.client as newbbl
21+
from bblfsh import role_id, role_name
22+
from bblfsh.node import Node
23+
from bblfsh.node_iterator import NodeIterator
24+
from bblfsh.result_context import ResultContext
25+
from bblfsh.aliases import (
26+
ParseRequest, ParseResponse, DriverStub, ProtocolServiceStub,
27+
VersionRequest, SupportedLanguagesRequest, ModeType,
28+
Mode, VersionResponse, DESCRIPTOR
29+
)
30+
from bblfsh.pyuast import uast, iterator as native_iterator
31+
from bblfsh.tree_order import TreeOrder
32+
33+
print("Warning: using deprecated bblfsh v1 compatibility layer.", file=sys.stderr)
34+
35+
36+
class WrongTypeException(Exception):
37+
"""
38+
This exception is raised when the API receives an unexpected type
39+
"""
40+
pass
41+
42+
43+
class CompatParseResponse:
44+
"""
45+
This class emulates the API of the old ParseResponse object.
46+
"""
47+
def __init__(self, ctx: ResultContext, filename: str = "") -> None:
48+
self._res_context = ctx
49+
self._filename = filename
50+
51+
@property
52+
def uast(self) -> Node:
53+
"""
54+
Returns the root Node.
55+
"""
56+
return self._res_context.uast
57+
58+
@property
59+
def ast(self) -> Node:
60+
"""
61+
Returns the root Node. This is provided for compatibility, but
62+
since the type of result is now expecified using CompatBblfshClient.parse
63+
or parse_native, it'll return the same as uast().
64+
"""
65+
return self._res_context.ast
66+
67+
@property
68+
def ctx(self) -> ResultContext:
69+
"""
70+
Returns the ResultContext of the response.
71+
"""
72+
return self._res_context
73+
74+
@property
75+
def elapsed(self) -> int:
76+
"""
77+
Provided for compatibility, but since the new API's ParseResponse doesn't
78+
provide an elapsed time it'll always return -1.
79+
"""
80+
# FIXME(juanjux): check if the caller can get this, or measure it ourselves.
81+
return -1
82+
83+
@property
84+
def language(self) -> str:
85+
"""
86+
Returns the language used for the request.
87+
"""
88+
return self._res_context.language
89+
90+
@property
91+
def filename(self) -> str:
92+
"""
93+
Returns the filename used for the request.
94+
"""
95+
return self._filename
96+
97+
@property
98+
def DESCRIPTOR(self) -> Any:
99+
"""
100+
Returns the gRPC context descriptor.
101+
"""
102+
return self._res_context.ctx.DESCRIPTOR
103+
104+
@property
105+
def errors(selfs) -> List:
106+
"""
107+
Provided for compatibility. Since the new API will raise exceptions on errors,
108+
this just returns and empty array.
109+
"""
110+
# ParseResponse would have raised an exception on errors
111+
return []
112+
113+
114+
class CompatBblfshClient:
115+
"""
116+
This emulates the methods and properties of the old BblfshClient.
117+
"""
118+
def __init__(self, endpoint: Union[str, grpc.Channel]) -> None:
119+
"""
120+
Connects to the specified grpc endpoint which can be specified either as
121+
a grpc Channel object or a connection string (like "0.0.0.0:6432").
122+
"""
123+
self._bblfsh_cli = newbbl.BblfshClient(endpoint)
124+
125+
self._channel = self._bblfsh_cli._channel
126+
self._stub_v1 = self._bblfsh_cli._stub_v1
127+
self._stub_v2 = self._bblfsh_cli._stub_v2
128+
129+
def _parse(self, filename: str, language: str = None, contents: str = None,
130+
timeout: float = None,
131+
mode: ModeType = Mode.Value('ANNOTATED')) -> CompatParseResponse:
132+
133+
if timeout is not None:
134+
timeout = int(timeout)
135+
136+
res = self._bblfsh_cli.parse(filename, language, contents,
137+
mode=mode, timeout=timeout)
138+
return CompatParseResponse(res, filename)
139+
140+
def parse(self, filename: str, language: str = None, contents: str = None,
141+
timeout: float = None) -> CompatParseResponse:
142+
143+
"""
144+
Parse the specified filename or contents and return a CompatParseResponse.
145+
"""
146+
147+
return self._parse(filename, language, contents, timeout,
148+
Mode.Value('ANNOTATED'))
149+
150+
def native_parse(self, filename: str, language: str = None,
151+
contents: str = None,
152+
timeout: float = None) -> CompatParseResponse:
153+
"""
154+
Same as parse() but the returned response will include only the native
155+
(non annotated) AST.
156+
"""
157+
158+
return self._parse(filename, language, contents, timeout,
159+
Mode.Value('NATIVE'))
160+
161+
def supported_languages(self) -> List[str]:
162+
"""
163+
Return a list of the languages that can be parsed by the connected
164+
endpoint (driver or bblfsh daemon).
165+
"""
166+
return self._bblfsh_cli.supported_languages()
167+
168+
def version(self) -> VersionResponse:
169+
"""
170+
Returns the connected endpoint version.
171+
"""
172+
return self._bblfsh_cli.version()
173+
174+
def close(self) -> None:
175+
"""
176+
Closes the connection to the endpoint.
177+
"""
178+
return self._bblfsh_cli.close()
179+
180+
181+
class CompatNodeIterator:
182+
"""
183+
This emulates the API of the pre-v3 iterators.
184+
"""
185+
def __init__(self, nodeit: NodeIterator, only_nodes: bool = False) -> None:
186+
"""
187+
Creates a CompatNodeIterator compatibility object using a NodeIterator
188+
from the post-v3 API. If the only_nodes parameter is set to true,
189+
scalars and strings won't be included in the results.
190+
"""
191+
self._nodeit = nodeit
192+
self._only_nodes = only_nodes
193+
# Used to forward calls of the old Node object
194+
self._last_node: Optional[Node] = None
195+
196+
def __iter__(self) -> 'CompatNodeIterator':
197+
return self
198+
199+
def __next__(self) -> Node:
200+
next_val = next(self._nodeit)
201+
202+
is_node = isinstance(next_val, Node)
203+
val = next_val.internal_node if is_node else next_val
204+
205+
# Skip positions and non dicts/lists, the later if only_nodes = True
206+
skip = False
207+
if isinstance(val, dict):
208+
if "@type" not in val or val["@type"] == "uast:Positions":
209+
skip = True
210+
elif self._only_nodes:
211+
skip = True
212+
213+
if skip:
214+
val = self.__next__().internal_node
215+
216+
ret_val = next_val if is_node else Node(value=val)
217+
self._last_node = ret_val
218+
return ret_val
219+
220+
def filter(self, query: str) -> Optional['CompatNodeIterator']:
221+
"""
222+
Further filter the results using this iterator as base.
223+
"""
224+
if not self._last_node:
225+
return None
226+
227+
return filter(self._last_node, query)
228+
229+
@property
230+
def properties(self) -> dict:
231+
"""
232+
Returns the properties of the current node in the iteration.
233+
"""
234+
if isinstance(self._last_node, dict):
235+
return self._last_node.keys()
236+
else:
237+
return {}
238+
239+
240+
def iterator(n: Union[Node, CompatNodeIterator, dict],
241+
order: TreeOrder = TreeOrder.PRE_ORDER) -> CompatNodeIterator:
242+
"""
243+
This function has the same signature as the pre-v3 iterator()
244+
call returning a compatibility CompatNodeIterator.
245+
"""
246+
247+
if isinstance(n, CompatNodeIterator):
248+
return CompatNodeIterator(n._nodeit.iterate(order), only_nodes=True)
249+
elif isinstance(n, Node):
250+
nat_it = native_iterator(n.internal_node, order)
251+
return CompatNodeIterator(NodeIterator(nat_it), only_nodes=True)
252+
elif isinstance(n, dict):
253+
nat_it = native_iterator(n, order)
254+
return CompatNodeIterator(NodeIterator(nat_it, uast()), only_nodes=True)
255+
else:
256+
raise WrongTypeException(
257+
"iterator on non node or iterator type (%s)" % str(type(n))
258+
)
259+
260+
261+
def filter(n: Node, query: str) -> CompatNodeIterator:
262+
"""
263+
This function has the same signature as the pre-v3 filter() returning a
264+
compatibility CompatNodeIterator.
265+
"""
266+
ctx = uast()
267+
return CompatNodeIterator(NodeIterator(ctx.filter(query, n.internal_node), ctx))
268+
269+
270+
def filter_nodes(n: Node, query: str) -> CompatNodeIterator:
271+
"""
272+
Utility function. Same as filter() but will only filter for nodes (i. e.
273+
it will exclude scalars and positions).
274+
"""
275+
return CompatNodeIterator(filter(n, query)._nodeit, only_nodes=True)
276+
277+
278+
class TypedQueryException(Exception):
279+
"""
280+
This exception will be raised when a query for a specific type (str, int, float...)
281+
returns a different type of more than one result.
282+
"""
283+
pass
284+
285+
286+
def _scalariter2item(n: Node, query: str, wanted_type: type) -> Any:
287+
rlist = list(filter(n, query))
288+
289+
if len(rlist) > 1:
290+
raise TypedQueryException("More than one result for %s typed query" % str(type))
291+
292+
value = rlist[0]
293+
if isinstance(value, Node):
294+
value = value.internal_node
295+
296+
value_type = type(value)
297+
if wanted_type == float and value_type == int:
298+
value = float(value)
299+
300+
if not isinstance(value, wanted_type):
301+
raise TypedQueryException("Typed query for type %s returned type %s instead"
302+
% (str(wanted_type), str(type(value))))
303+
304+
return wanted_type(value)
305+
306+
307+
def filter_string(n: Node, query: str) -> str:
308+
"""
309+
Filter and ensure that the returned value is of string type.
310+
"""
311+
return _scalariter2item(n, query, str)
312+
313+
314+
def filter_bool(n: Node, query: str) -> bool:
315+
"""
316+
Filter and ensure that the returned value is of type bool.
317+
"""
318+
return _scalariter2item(n, query, bool)
319+
320+
321+
def filter_int(n: Node, query: str) -> int:
322+
"""
323+
Filter and ensure that the returned value is of type int.
324+
"""
325+
return _scalariter2item(n, query, int)
326+
327+
328+
def filter_float(n: Node, query: str) -> float:
329+
"""
330+
Filter and ensure that the returned value is of type int.
331+
"""
332+
return _scalariter2item(n, query, float)
333+
334+
335+
filter_number = filter_float

0 commit comments

Comments
 (0)