Browse Source
Issue #28038: Remove Tools/parser/com2ann.py and its unit test.
Issue #28038: Remove Tools/parser/com2ann.py and its unit test.
Development is moving to https://github.com/ilevkivskyi/com2annpull/9921/head
2 changed files with 0 additions and 568 deletions
@ -1,260 +0,0 @@ |
|||
"""Tests for the com2ann.py script in the Tools/parser directory.""" |
|||
|
|||
import unittest |
|||
import test.support |
|||
import os |
|||
import re |
|||
|
|||
from test.test_tools import basepath, toolsdir, skip_if_missing |
|||
|
|||
skip_if_missing() |
|||
|
|||
parser_path = os.path.join(toolsdir, "parser") |
|||
|
|||
with test.support.DirsOnSysPath(parser_path): |
|||
from com2ann import * |
|||
|
|||
class BaseTestCase(unittest.TestCase): |
|||
|
|||
def check(self, code, expected, n=False, e=False): |
|||
self.assertEqual(com2ann(code, |
|||
drop_None=n, drop_Ellipsis=e, silent=True), |
|||
expected) |
|||
|
|||
class SimpleTestCase(BaseTestCase): |
|||
# Tests for basic conversions |
|||
|
|||
def test_basics(self): |
|||
self.check("z = 5", "z = 5") |
|||
self.check("z: int = 5", "z: int = 5") |
|||
self.check("z = 5 # type: int", "z: int = 5") |
|||
self.check("z = 5 # type: int # comment", |
|||
"z: int = 5 # comment") |
|||
|
|||
def test_type_ignore(self): |
|||
self.check("foobar = foobaz() #type: ignore", |
|||
"foobar = foobaz() #type: ignore") |
|||
self.check("a = 42 #type: ignore #comment", |
|||
"a = 42 #type: ignore #comment") |
|||
|
|||
def test_complete_tuple(self): |
|||
self.check("t = 1, 2, 3 # type: Tuple[int, ...]", |
|||
"t: Tuple[int, ...] = (1, 2, 3)") |
|||
self.check("t = 1, # type: Tuple[int]", |
|||
"t: Tuple[int] = (1,)") |
|||
self.check("t = (1, 2, 3) # type: Tuple[int, ...]", |
|||
"t: Tuple[int, ...] = (1, 2, 3)") |
|||
|
|||
def test_drop_None(self): |
|||
self.check("x = None # type: int", |
|||
"x: int", True) |
|||
self.check("x = None # type: int # another", |
|||
"x: int # another", True) |
|||
self.check("x = None # type: int # None", |
|||
"x: int # None", True) |
|||
|
|||
def test_drop_Ellipsis(self): |
|||
self.check("x = ... # type: int", |
|||
"x: int", False, True) |
|||
self.check("x = ... # type: int # another", |
|||
"x: int # another", False, True) |
|||
self.check("x = ... # type: int # ...", |
|||
"x: int # ...", False, True) |
|||
|
|||
def test_newline(self): |
|||
self.check("z = 5 # type: int\r\n", "z: int = 5\r\n") |
|||
self.check("z = 5 # type: int # comment\x85", |
|||
"z: int = 5 # comment\x85") |
|||
|
|||
def test_wrong(self): |
|||
self.check("#type : str", "#type : str") |
|||
self.check("x==y #type: bool", "x==y #type: bool") |
|||
|
|||
def test_pattern(self): |
|||
for line in ["#type: int", " # type: str[:] # com"]: |
|||
self.assertTrue(re.search(TYPE_COM, line)) |
|||
for line in ["", "#", "# comment", "#type", "type int:"]: |
|||
self.assertFalse(re.search(TYPE_COM, line)) |
|||
|
|||
class BigTestCase(BaseTestCase): |
|||
# Tests for really crazy formatting, to be sure |
|||
# that script works reasonably in extreme situations |
|||
|
|||
def test_crazy(self): |
|||
self.maxDiff = None |
|||
self.check(crazy_code, big_result, False, False) |
|||
self.check(crazy_code, big_result_ne, True, True) |
|||
|
|||
crazy_code = """\ |
|||
# -*- coding: utf-8 -*- # this should not be spoiled |
|||
''' |
|||
Docstring here |
|||
''' |
|||
|
|||
import testmod |
|||
x = 5 #type : int # this one is OK |
|||
ttt \\ |
|||
= \\ |
|||
1.0, \\ |
|||
2.0, \\ |
|||
3.0, #type: Tuple[float, float, float] |
|||
with foo(x==1) as f: #type: str |
|||
print(f) |
|||
|
|||
for i, j in my_inter(x=1): # type: ignore |
|||
i + j # type: int # what about this |
|||
|
|||
x = y = z = 1 # type: int |
|||
x, y, z = [], [], [] # type: (List[int], List[int], List[str]) |
|||
class C: |
|||
|
|||
|
|||
l[f(x |
|||
=1)] = [ |
|||
|
|||
1, |
|||
2, |
|||
] # type: List[int] |
|||
|
|||
|
|||
(C.x[1]) = \\ |
|||
42 == 5# type: bool |
|||
lst[...] = \\ |
|||
((\\ |
|||
...)) # type: int # comment .. |
|||
|
|||
y = ... # type: int # comment ... |
|||
z = ... |
|||
#type: int |
|||
|
|||
|
|||
#DONE placement of annotation after target rather than before = |
|||
|
|||
TD.x[1] \\ |
|||
= 0 == 5# type: bool |
|||
|
|||
TD.y[1] =5 == 5# type: bool # one more here |
|||
F[G(x == y, |
|||
|
|||
# hm... |
|||
|
|||
z)]\\ |
|||
= None # type: OMG[int] # comment: None |
|||
x = None#type:int #comment : None""" |
|||
|
|||
big_result = """\ |
|||
# -*- coding: utf-8 -*- # this should not be spoiled |
|||
''' |
|||
Docstring here |
|||
''' |
|||
|
|||
import testmod |
|||
x: int = 5 # this one is OK |
|||
ttt: Tuple[float, float, float] \\ |
|||
= \\ |
|||
(1.0, \\ |
|||
2.0, \\ |
|||
3.0,) |
|||
with foo(x==1) as f: #type: str |
|||
print(f) |
|||
|
|||
for i, j in my_inter(x=1): # type: ignore |
|||
i + j # type: int # what about this |
|||
|
|||
x = y = z = 1 # type: int |
|||
x, y, z = [], [], [] # type: (List[int], List[int], List[str]) |
|||
class C: |
|||
|
|||
|
|||
l[f(x |
|||
=1)]: List[int] = [ |
|||
|
|||
1, |
|||
2, |
|||
] |
|||
|
|||
|
|||
(C.x[1]): bool = \\ |
|||
42 == 5 |
|||
lst[...]: int = \\ |
|||
((\\ |
|||
...)) # comment .. |
|||
|
|||
y: int = ... # comment ... |
|||
z = ... |
|||
#type: int |
|||
|
|||
|
|||
#DONE placement of annotation after target rather than before = |
|||
|
|||
TD.x[1]: bool \\ |
|||
= 0 == 5 |
|||
|
|||
TD.y[1]: bool =5 == 5 # one more here |
|||
F[G(x == y, |
|||
|
|||
# hm... |
|||
|
|||
z)]: OMG[int]\\ |
|||
= None # comment: None |
|||
x: int = None #comment : None""" |
|||
|
|||
big_result_ne = """\ |
|||
# -*- coding: utf-8 -*- # this should not be spoiled |
|||
''' |
|||
Docstring here |
|||
''' |
|||
|
|||
import testmod |
|||
x: int = 5 # this one is OK |
|||
ttt: Tuple[float, float, float] \\ |
|||
= \\ |
|||
(1.0, \\ |
|||
2.0, \\ |
|||
3.0,) |
|||
with foo(x==1) as f: #type: str |
|||
print(f) |
|||
|
|||
for i, j in my_inter(x=1): # type: ignore |
|||
i + j # type: int # what about this |
|||
|
|||
x = y = z = 1 # type: int |
|||
x, y, z = [], [], [] # type: (List[int], List[int], List[str]) |
|||
class C: |
|||
|
|||
|
|||
l[f(x |
|||
=1)]: List[int] = [ |
|||
|
|||
1, |
|||
2, |
|||
] |
|||
|
|||
|
|||
(C.x[1]): bool = \\ |
|||
42 == 5 |
|||
lst[...]: int \\ |
|||
\\ |
|||
# comment .. |
|||
|
|||
y: int # comment ... |
|||
z = ... |
|||
#type: int |
|||
|
|||
|
|||
#DONE placement of annotation after target rather than before = |
|||
|
|||
TD.x[1]: bool \\ |
|||
= 0 == 5 |
|||
|
|||
TD.y[1]: bool =5 == 5 # one more here |
|||
F[G(x == y, |
|||
|
|||
# hm... |
|||
|
|||
z)]: OMG[int]\\ |
|||
# comment: None |
|||
x: int #comment : None""" |
|||
|
|||
if __name__ == '__main__': |
|||
unittest.main() |
|||
@ -1,308 +0,0 @@ |
|||
"""Helper module to tranlate 3.5 type comments to 3.6 variable annotations.""" |
|||
import re |
|||
import os |
|||
import ast |
|||
import argparse |
|||
import tokenize |
|||
from collections import defaultdict |
|||
from textwrap import dedent |
|||
from io import BytesIO |
|||
|
|||
__all__ = ['com2ann', 'TYPE_COM'] |
|||
|
|||
TYPE_COM = re.compile(r'\s*#\s*type\s*:.*$', flags=re.DOTALL) |
|||
TRAIL_OR_COM = re.compile(r'\s*$|\s*#.*$', flags=re.DOTALL) |
|||
|
|||
|
|||
class _Data: |
|||
"""Internal class describing global data on file.""" |
|||
def __init__(self, lines, tokens): |
|||
self.lines = lines |
|||
self.tokens = tokens |
|||
ttab = defaultdict(list) # maps line number to token numbers |
|||
for i, tok in enumerate(tokens): |
|||
ttab[tok.start[0]].append(i) |
|||
self.ttab = ttab |
|||
self.success = [] # list of lines where type comments where processed |
|||
self.fail = [] # list of lines where type comments where rejected |
|||
|
|||
|
|||
def skip_blank(d, lno): |
|||
while d.lines[lno].strip() == '': |
|||
lno += 1 |
|||
return lno |
|||
|
|||
|
|||
def find_start(d, lcom): |
|||
"""Find first char of the assignment target.""" |
|||
i = d.ttab[lcom + 1][-2] # index of type comment token in tokens list |
|||
while ((d.tokens[i].exact_type != tokenize.NEWLINE) and |
|||
(d.tokens[i].exact_type != tokenize.ENCODING)): |
|||
i -= 1 |
|||
lno = d.tokens[i].start[0] |
|||
return skip_blank(d, lno) |
|||
|
|||
|
|||
def check_target(stmt): |
|||
if len(stmt.body): |
|||
assign = stmt.body[0] |
|||
else: |
|||
return False |
|||
if isinstance(assign, ast.Assign) and len(assign.targets) == 1: |
|||
targ = assign.targets[0] |
|||
else: |
|||
return False |
|||
if (isinstance(targ, ast.Name) or isinstance(targ, ast.Attribute) |
|||
or isinstance(targ, ast.Subscript)): |
|||
return True |
|||
return False |
|||
|
|||
|
|||
def find_eq(d, lstart): |
|||
"""Find equal sign starting from lstart taking care about d[f(x=1)] = 5.""" |
|||
col = pars = 0 |
|||
lno = lstart |
|||
while d.lines[lno][col] != '=' or pars != 0: |
|||
ch = d.lines[lno][col] |
|||
if ch in '([{': |
|||
pars += 1 |
|||
elif ch in ')]}': |
|||
pars -= 1 |
|||
if ch == '#' or col == len(d.lines[lno])-1: |
|||
lno = skip_blank(d, lno+1) |
|||
col = 0 |
|||
else: |
|||
col += 1 |
|||
return lno, col |
|||
|
|||
|
|||
def find_val(d, poseq): |
|||
"""Find position of first char of assignment value starting from poseq.""" |
|||
lno, col = poseq |
|||
while (d.lines[lno][col].isspace() or d.lines[lno][col] in '=\\'): |
|||
if col == len(d.lines[lno])-1: |
|||
lno += 1 |
|||
col = 0 |
|||
else: |
|||
col += 1 |
|||
return lno, col |
|||
|
|||
|
|||
def find_targ(d, poseq): |
|||
"""Find position of last char of target (annotation goes here).""" |
|||
lno, col = poseq |
|||
while (d.lines[lno][col].isspace() or d.lines[lno][col] in '=\\'): |
|||
if col == 0: |
|||
lno -= 1 |
|||
col = len(d.lines[lno])-1 |
|||
else: |
|||
col -= 1 |
|||
return lno, col+1 |
|||
|
|||
|
|||
def trim(new_lines, string, ltarg, poseq, lcom, ccom): |
|||
"""Remove None or Ellipsis from assignment value. |
|||
|
|||
Also remove parens if one has (None), (...) etc. |
|||
string -- 'None' or '...' |
|||
ltarg -- line where last char of target is located |
|||
poseq -- position of equal sign |
|||
lcom, ccom -- position of type comment |
|||
""" |
|||
nopars = lambda s: s.replace('(', '').replace(')', '') |
|||
leq, ceq = poseq |
|||
end = ccom if leq == lcom else len(new_lines[leq]) |
|||
subline = new_lines[leq][:ceq] |
|||
if leq == ltarg: |
|||
subline = subline.rstrip() |
|||
new_lines[leq] = subline + (new_lines[leq][end:] if leq == lcom |
|||
else new_lines[leq][ceq+1:end]) |
|||
|
|||
for lno in range(leq+1,lcom): |
|||
new_lines[lno] = nopars(new_lines[lno]) |
|||
|
|||
if lcom != leq: |
|||
subline = nopars(new_lines[lcom][:ccom]).replace(string, '') |
|||
if (not subline.isspace()): |
|||
subline = subline.rstrip() |
|||
new_lines[lcom] = subline + new_lines[lcom][ccom:] |
|||
|
|||
|
|||
def _com2ann(d, drop_None, drop_Ellipsis): |
|||
new_lines = d.lines[:] |
|||
for lcom, line in enumerate(d.lines): |
|||
match = re.search(TYPE_COM, line) |
|||
if match: |
|||
# strip " # type : annotation \n" -> "annotation \n" |
|||
tp = match.group().lstrip()[1:].lstrip()[4:].lstrip()[1:].lstrip() |
|||
submatch = re.search(TRAIL_OR_COM, tp) |
|||
subcom = '' |
|||
if submatch and submatch.group(): |
|||
subcom = submatch.group() |
|||
tp = tp[:submatch.start()] |
|||
if tp == 'ignore': |
|||
continue |
|||
ccom = match.start() |
|||
if not any(d.tokens[i].exact_type == tokenize.COMMENT |
|||
for i in d.ttab[lcom + 1]): |
|||
d.fail.append(lcom) |
|||
continue # type comment inside string |
|||
lstart = find_start(d, lcom) |
|||
stmt_str = dedent(''.join(d.lines[lstart:lcom+1])) |
|||
try: |
|||
stmt = ast.parse(stmt_str) |
|||
except SyntaxError: |
|||
d.fail.append(lcom) |
|||
continue # for or with statements |
|||
if not check_target(stmt): |
|||
d.fail.append(lcom) |
|||
continue |
|||
|
|||
d.success.append(lcom) |
|||
val = stmt.body[0].value |
|||
|
|||
# writing output now |
|||
poseq = find_eq(d, lstart) |
|||
lval, cval = find_val(d, poseq) |
|||
ltarg, ctarg = find_targ(d, poseq) |
|||
|
|||
op_par = '' |
|||
cl_par = '' |
|||
if isinstance(val, ast.Tuple): |
|||
if d.lines[lval][cval] != '(': |
|||
op_par = '(' |
|||
cl_par = ')' |
|||
# write the comment first |
|||
new_lines[lcom] = d.lines[lcom][:ccom].rstrip() + cl_par + subcom |
|||
ccom = len(d.lines[lcom][:ccom].rstrip()) |
|||
|
|||
string = False |
|||
if isinstance(val, ast.Tuple): |
|||
# t = 1, 2 -> t = (1, 2); only latter is allowed with annotation |
|||
free_place = int(new_lines[lval][cval-2:cval] == ' ') |
|||
new_lines[lval] = (new_lines[lval][:cval-free_place] + |
|||
op_par + new_lines[lval][cval:]) |
|||
elif isinstance(val, ast.Ellipsis) and drop_Ellipsis: |
|||
string = '...' |
|||
elif (isinstance(val, ast.NameConstant) and |
|||
val.value is None and drop_None): |
|||
string = 'None' |
|||
if string: |
|||
trim(new_lines, string, ltarg, poseq, lcom, ccom) |
|||
|
|||
# finally write an annotation |
|||
new_lines[ltarg] = (new_lines[ltarg][:ctarg] + |
|||
': ' + tp + new_lines[ltarg][ctarg:]) |
|||
return ''.join(new_lines) |
|||
|
|||
|
|||
def com2ann(code, *, drop_None=False, drop_Ellipsis=False, silent=False): |
|||
"""Translate type comments to type annotations in code. |
|||
|
|||
Take code as string and return this string where:: |
|||
|
|||
variable = value # type: annotation # real comment |
|||
|
|||
is translated to:: |
|||
|
|||
variable: annotation = value # real comment |
|||
|
|||
For unsupported syntax cases, the type comments are |
|||
left intact. If drop_None is True or if drop_Ellipsis |
|||
is True translate correcpondingly:: |
|||
|
|||
variable = None # type: annotation |
|||
variable = ... # type: annotation |
|||
|
|||
into:: |
|||
|
|||
variable: annotation |
|||
|
|||
The tool tries to preserve code formatting as much as |
|||
possible, but an exact translation is not guarateed. |
|||
A summary of translated comments id printed by default. |
|||
""" |
|||
try: |
|||
ast.parse(code) # we want to work only with file without syntax errors |
|||
except SyntaxError: |
|||
return None |
|||
lines = code.splitlines(keepends=True) |
|||
rl = BytesIO(code.encode('utf-8')).readline |
|||
tokens = list(tokenize.tokenize(rl)) |
|||
|
|||
data = _Data(lines, tokens) |
|||
new_code = _com2ann(data, drop_None, drop_Ellipsis) |
|||
|
|||
if not silent: |
|||
if data.success: |
|||
print('Comments translated on lines:', |
|||
', '.join(str(lno+1) for lno in data.success)) |
|||
if data.fail: |
|||
print('Comments rejected on lines:', |
|||
', '.join(str(lno+1) for lno in data.fail)) |
|||
if not data.success and not data.fail: |
|||
print('No type comments found') |
|||
|
|||
return new_code |
|||
|
|||
|
|||
def translate_file(infile, outfile, dnone, dell, silent): |
|||
try: |
|||
descr = tokenize.open(infile) |
|||
except SyntaxError: |
|||
print("Cannot open", infile) |
|||
return |
|||
with descr as f: |
|||
code = f.read() |
|||
enc = f.encoding |
|||
if not silent: |
|||
print('File:', infile) |
|||
new_code = com2ann(code, drop_None=dnone, |
|||
drop_Ellipsis=dell, |
|||
silent=silent) |
|||
if new_code is None: |
|||
print("SyntaxError in", infile) |
|||
return |
|||
with open(outfile, 'wb') as f: |
|||
f.write((new_code).encode(enc)) |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
|
|||
parser = argparse.ArgumentParser(description=__doc__) |
|||
parser.add_argument("-o", "--outfile", |
|||
help="output file, will be overwritten if exists,\n" |
|||
"defaults to input file") |
|||
parser.add_argument("infile", |
|||
help="input file or directory for translation, must\n" |
|||
"contain no syntax errors, for directory\n" |
|||
"the outfile is ignored and translation is\n" |
|||
"made in place") |
|||
parser.add_argument("-s", "--silent", |
|||
help="Do not print summary for line numbers of\n" |
|||
"translated and rejected comments", |
|||
action="store_true") |
|||
parser.add_argument("-n", "--drop-none", |
|||
help="drop any None as assignment value during\n" |
|||
"translation if it is annotated by a type coment", |
|||
action="store_true") |
|||
parser.add_argument("-e", "--drop-ellipsis", |
|||
help="drop any Ellipsis (...) as assignment value during\n" |
|||
"translation if it is annotated by a type coment", |
|||
action="store_true") |
|||
args = parser.parse_args() |
|||
if args.outfile is None: |
|||
args.outfile = args.infile |
|||
|
|||
if os.path.isfile(args.infile): |
|||
translate_file(args.infile, args.outfile, |
|||
args.drop_none, args.drop_ellipsis, args.silent) |
|||
else: |
|||
for root, dirs, files in os.walk(args.infile): |
|||
for afile in files: |
|||
_, ext = os.path.splitext(afile) |
|||
if ext == '.py' or ext == '.pyi': |
|||
fname = os.path.join(root, afile) |
|||
translate_file(fname, fname, |
|||
args.drop_none, args.drop_ellipsis, |
|||
args.silent) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue