Skip to content

Commit c658f55

Browse files
committed
Add support for forward-reference typings (PEP-484#forward-references)
Implementation: A context tracker monitors the incomping token stream and reports whether we are currently in a typing context or not, if we are in a typing context then the normal replacement code treats string values as a new token string to unasyncify recursively.
1 parent 6e72df4 commit c658f55

File tree

3 files changed

+109
-3
lines changed

3 files changed

+109
-3
lines changed

Diff for: src/unasync/__init__.py

+79-3
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,57 @@ def _unasync_file(self, filepath):
100100
def _unasync_tokens(self, tokens):
101101
# TODO __await__, ...?
102102
used_space = None
103+
context = None # Can be `None`, `"func_decl"`, `"func_name"`, `"arg_list"`, `"arg_list_end"`, `"return_type"`
104+
brace_depth = 0
105+
typing_ctx = False
106+
103107
for space, toknum, tokval in tokens:
108+
# Update context state tracker
109+
if context is None and toknum == std_tokenize.NAME and tokval == "def":
110+
context = "func_decl"
111+
elif context == "func_decl" and toknum == std_tokenize.NAME:
112+
context = "func_name"
113+
elif context == "func_name" and toknum == std_tokenize.OP and tokval == "(":
114+
context = "arg_list"
115+
elif context == "arg_list":
116+
if toknum == std_tokenize.OP and tokval in ("(", "["):
117+
brace_depth += 1
118+
elif (
119+
toknum == std_tokenize.OP
120+
and tokval in (")", "]")
121+
and brace_depth >= 1
122+
):
123+
brace_depth -= 1
124+
elif toknum == std_tokenize.OP and tokval == ")":
125+
context = "arg_list_end"
126+
elif toknum == std_tokenize.OP and tokval == ":" and brace_depth < 1:
127+
typing_ctx = True
128+
elif toknum == std_tokenize.OP and tokval == "," and brace_depth < 1:
129+
typing_ctx = False
130+
elif (
131+
context == "arg_list_end"
132+
and toknum == std_tokenize.OP
133+
and tokval == "->"
134+
):
135+
context = "return_type"
136+
typing_ctx = True
137+
elif context == "return_type":
138+
if toknum == std_tokenize.OP and tokval in ("(", "["):
139+
brace_depth += 1
140+
elif (
141+
toknum == std_tokenize.OP
142+
and tokval in (")", "]")
143+
and brace_depth >= 1
144+
):
145+
brace_depth -= 1
146+
elif toknum == std_tokenize.OP and tokval == ":":
147+
context = None
148+
typing_ctx = False
149+
else: # Something unexpected happend - reset state
150+
context = None
151+
brace_depth = 0
152+
typing_ctx = False
153+
104154
if tokval in ["async", "await"]:
105155
# When removing async or await, we want to use the whitespace that
106156
# was before async/await before the next token so that
@@ -111,8 +161,34 @@ def _unasync_tokens(self, tokens):
111161
if toknum == std_tokenize.NAME:
112162
tokval = self._unasync_name(tokval)
113163
elif toknum == std_tokenize.STRING:
114-
left_quote, name, right_quote = tokval[0], tokval[1:-1], tokval[-1]
115-
tokval = left_quote + self._unasync_name(name) + right_quote
164+
# Strings in typing context are forward-references and should be unasyncified
165+
quote = ""
166+
prefix = ""
167+
while ord(tokval[0]) in range(ord("a"), ord("z") + 1):
168+
prefix += tokval[0]
169+
tokval = tokval[1:]
170+
171+
if tokval.startswith('"""') and tokval.endswith('"""'):
172+
quote = '"""' # Broken syntax highlighters workaround: """
173+
elif tokval.startswith("'''") and tokval.endswith("'''"):
174+
quote = "'''" # Broken syntax highlighters wokraround: '''
175+
elif tokval.startswith('"') and tokval.endswith('"'):
176+
quote = '"'
177+
elif tokval.startswith("'") and tokval.endswith(
178+
"'"
179+
): # pragma: no branch
180+
quote = "'"
181+
assert (
182+
len(quote) > 0
183+
), "Quoting style of string {0!r} unknown".format(tokval)
184+
stringval = tokval[len(quote) : -len(quote)]
185+
if typing_ctx:
186+
stringval = _untokenize(
187+
self._unasync_tokens(_tokenize(io.StringIO(stringval)))
188+
)
189+
else:
190+
stringval = self._unasync_name(stringval)
191+
tokval = prefix + quote + stringval + quote
116192
elif toknum == std_tokenize.COMMENT and tokval.startswith(
117193
_TYPE_COMMENT_PREFIX
118194
):
@@ -190,7 +266,7 @@ def _tokenize(f):
190266
last_end = (1, 0)
191267
for tok in _get_tokens(f):
192268
if last_end[0] < tok.start[0]:
193-
yield ("", std_tokenize.STRING, " \\\n")
269+
yield (" ", std_tokenize.NEWLINE, "\\\n")
194270
last_end = (tok.start[0], 0)
195271

196272
space = ""

Diff for: tests/data/async/typing.py

+15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ async def func2(a): # type: (typing.AsyncIterable[int]) -> str
1818
return str(b)
1919

2020

21+
# fmt: off
22+
# A forward-reference typed function that returns an iterator for an (a)sync iterable
23+
async def aiter1(a: "typing.AsyncIterable[int]") -> 'typing.AsyncIterable[int]':
24+
return a.__aiter__()
25+
26+
# Same as the above but using tripple-quoted strings
27+
async def aiter2(a: """typing.AsyncIterable[int]""") -> r'''typing.AsyncIterable[int]''':
28+
return a.__aiter__()
29+
30+
# Same as the above but without forward-references
31+
async def aiter3(a: typing.AsyncIterable[int]) -> typing.AsyncIterable[int]:
32+
return a.__aiter__()
33+
# fmt: on
34+
35+
2136
# And some funky edge cases to at least cover the relevant at all in this test
2237
a: int = 5
2338
b: str = a # type: ignore # This the actual comment and the type declaration silences the warning that would otherwise happen

Diff for: tests/data/sync/typing.py

+15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ def func2(a): # type: (typing.Iterable[int]) -> str
1818
return str(b)
1919

2020

21+
# fmt: off
22+
# A forward-reference typed function that returns an iterator for an (a)sync iterable
23+
def aiter1(a: "typing.Iterable[int]") -> 'typing.Iterable[int]':
24+
return a.__iter__()
25+
26+
# Same as the above but using tripple-quoted strings
27+
def aiter2(a: """typing.Iterable[int]""") -> r'''typing.Iterable[int]''':
28+
return a.__iter__()
29+
30+
# Same as the above but without forward-references
31+
def aiter3(a: typing.Iterable[int]) -> typing.Iterable[int]:
32+
return a.__iter__()
33+
# fmt: on
34+
35+
2136
# And some funky edge cases to at least cover the relevant at all in this test
2237
a: int = 5
2338
b: str = a # type: ignore # This the actual comment and the type declaration silences the warning that would otherwise happen

0 commit comments

Comments
 (0)