mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-06-04 06:48:27 -04:00
4a6afd469f
0.4 development continues at /sqlalchemy/branches/rel_0_4
240 lines
7.3 KiB
Python
240 lines
7.3 KiB
Python
"""A collection of Python source transformers.
|
|
|
|
Supports the 'clone' command, providing source code transforms to run the test
|
|
suite on pre Python 2.4-level parser implementations.
|
|
|
|
Includes::
|
|
|
|
py23
|
|
Converts 2.4-level source code into 2.3-parsable source.
|
|
Currently only rewrites @decorators, but generator transformations
|
|
are possible.
|
|
py23_decorators
|
|
py23 is currently an alias for py23_decorators.
|
|
"""
|
|
|
|
import sys
|
|
from tokenize import generate_tokens, INDENT, DEDENT, NAME, OP, NL, NEWLINE, \
|
|
NUMBER, STRING, COMMENT
|
|
|
|
__all__ = ['py23_decorators', 'py23']
|
|
|
|
|
|
def py23_decorators(lines):
|
|
"""Translates @decorators in source lines to 2.3 syntax."""
|
|
|
|
tokens = peekable(generate_tokens(iter(lines).next))
|
|
text = untokenize(backport_decorators(tokens))
|
|
return [x + '\n' for x in text.split('\n')]
|
|
|
|
py23 = py23_decorators
|
|
|
|
|
|
def backport_decorators(stream):
|
|
"""Restates @decorators in 2.3 syntax
|
|
|
|
Operates on token streams. Converts::
|
|
|
|
@foo
|
|
@bar(1, 2)
|
|
def quux():
|
|
pass
|
|
into::
|
|
|
|
def quux():
|
|
pass
|
|
quux = bar(1, 2)(quux)
|
|
quux = foo(quux)
|
|
|
|
Fails on decorated one-liners::
|
|
|
|
@decorator
|
|
def fn(): pass
|
|
"""
|
|
|
|
if not hasattr(stream, 'peek'):
|
|
stream = peekable(iter(stream))
|
|
|
|
stack = [_DecoratorState('')]
|
|
emit = []
|
|
for ttype, tok, _, _, _ in stream:
|
|
current = stack[-1]
|
|
if ttype == INDENT:
|
|
current = _DecoratorState(tok)
|
|
stack.append(current)
|
|
elif ttype == DEDENT:
|
|
previous = stack.pop()
|
|
assert not previous.decorations
|
|
current = stack[-1]
|
|
if current.decorations:
|
|
ws = pop_trailing_whitespace(emit)
|
|
|
|
emit.append((ttype, tok))
|
|
for decorator, misc in reversed(current.decorations):
|
|
if not decorator or decorator[0][1] != '@':
|
|
emit.extend(decorator)
|
|
else:
|
|
emit.extend(
|
|
[(NAME, current.fn_name), (OP, '=')] +
|
|
decorator[1:] +
|
|
[(OP, '('), (NAME, current.fn_name), (OP, ')')])
|
|
emit.extend(misc)
|
|
current.decorations = []
|
|
emit.extend(ws)
|
|
continue
|
|
elif ttype == OP and tok == '@':
|
|
current.in_decorator = True
|
|
decoration = [(ttype, tok)]
|
|
current.decorations.append((decoration, []))
|
|
current.consume_identifier(stream)
|
|
if stream.peek()[1] == '(':
|
|
current.consume_parened(stream)
|
|
continue
|
|
elif ttype == NAME and tok == 'def':
|
|
current.in_decorator = False
|
|
current.fn_name = stream.peek()[1]
|
|
elif current.in_decorator:
|
|
current.append_misc((ttype, tok))
|
|
continue
|
|
|
|
emit.append((ttype, tok))
|
|
return emit
|
|
|
|
class _DecoratorState(object):
|
|
"""Holds state for restating decorators as function calls."""
|
|
|
|
in_decorator = False
|
|
fn_name = None
|
|
def __init__(self, indent):
|
|
self.indent = indent
|
|
self.decorations = []
|
|
def append_misc(self, token):
|
|
if not self.decorations:
|
|
self.decorations.append(([], []))
|
|
self.decorations[-1][1].append(token)
|
|
def consume_identifier(self, stream):
|
|
while True:
|
|
typ, value = stream.peek()[:2]
|
|
if not (typ == NAME or (typ == OP and value == '.')):
|
|
break
|
|
self.decorations[-1][0].append(stream.next()[:2])
|
|
def consume_parened(self, stream):
|
|
"""Consume a (paren) sequence from a token seq starting with ("""
|
|
depth, offsets = 0, {'(':1, ')':-1}
|
|
while True:
|
|
typ, value = stream.next()[:2]
|
|
if typ == OP:
|
|
depth += offsets.get(value, 0)
|
|
self.decorations[-1][0].append((typ, value))
|
|
if depth == 0:
|
|
break
|
|
|
|
def pop_trailing_whitespace(tokens):
|
|
"""Removes trailing whitespace tokens from a token list."""
|
|
|
|
popped = []
|
|
for token in reversed(list(tokens)):
|
|
if token[0] not in (NL, COMMENT):
|
|
break
|
|
popped.append(tokens.pop())
|
|
return popped
|
|
|
|
def untokenize(iterable):
|
|
"""Turns a stream of tokens into a Python source str.
|
|
|
|
A PEP-8-ish variant of Python 2.5+'s tokenize.untokenize. Produces output
|
|
that's not perfect, but is at least readable. The stdlib version is
|
|
basically unusable.
|
|
"""
|
|
|
|
if not hasattr(iterable, 'peek'):
|
|
iterable = peekable(iter(iterable))
|
|
|
|
startline = False
|
|
indents = []
|
|
toks = []
|
|
toks_append = toks.append
|
|
|
|
# this is pretty roughly hacked. i think it could get very close to
|
|
# perfect by rewriting to operate over a sliding window of
|
|
# (prev, current, next) token sets + making some grouping macros to
|
|
# include all the tokens and operators this omits.
|
|
for tok in iterable:
|
|
toknum, tokval = tok[:2]
|
|
|
|
try:
|
|
next_num, next_val = iterable.peek()[:2]
|
|
except StopIteration:
|
|
next_num, next_val = None, None
|
|
|
|
if toknum == NAME:
|
|
if tokval == 'in':
|
|
tokval += ' '
|
|
elif next_num == OP:
|
|
if next_val not in ('(', ')', '[', ']', '{', '}',
|
|
':', '.', ',',):
|
|
tokval += ' '
|
|
elif next_num != NEWLINE:
|
|
tokval += ' '
|
|
elif toknum == OP:
|
|
if tokval in ('(', '@', '.', '[', '{', '*', '**'):
|
|
pass
|
|
elif tokval in ('%', ':') and next_num not in (NEWLINE, ):
|
|
tokval += ' '
|
|
elif next_num in (NAME, COMMENT,
|
|
NUMBER, STRING):
|
|
tokval += ' '
|
|
elif (tokval in (')', ']', '}') and next_num == OP and
|
|
'=' in next_val):
|
|
tokval += ' '
|
|
elif tokval == ',' or '=' in tokval:
|
|
tokval += ' '
|
|
elif toknum in (NUMBER, STRING):
|
|
if next_num == OP and next_val not in (')', ']', '}', ',', ':'):
|
|
tokval += ' '
|
|
elif next_num == NAME:
|
|
tokval += ' '
|
|
|
|
# would be nice to indent continued lines...
|
|
if toknum == INDENT:
|
|
indents.append(tokval)
|
|
continue
|
|
elif toknum == DEDENT:
|
|
indents.pop()
|
|
continue
|
|
elif toknum in (NEWLINE, COMMENT, NL):
|
|
startline = True
|
|
elif startline and indents:
|
|
toks_append(indents[-1])
|
|
startline = False
|
|
toks_append(tokval)
|
|
return ''.join(toks)
|
|
|
|
|
|
class peekable(object):
|
|
"""A iterator wrapper that allows peek()ing at the next value."""
|
|
|
|
def __init__(self, iterator):
|
|
self.iterator = iterator
|
|
self.buffer = []
|
|
def next(self):
|
|
if self.buffer:
|
|
return self.buffer.pop(0)
|
|
return self.iterator.next()
|
|
def peek(self):
|
|
if self.buffer:
|
|
return self.buffer[0]
|
|
x = self.iterator.next()
|
|
self.buffer.append(x)
|
|
return x
|
|
def __iter__(self):
|
|
return self
|
|
|
|
if __name__ == '__main__':
|
|
# runnable. converts a named file to 2.3.
|
|
input = open(len(sys.argv) == 2 and sys.argv[1] or __file__)
|
|
|
|
tokens = generate_tokens(input.readline)
|
|
back = backport_decorators(tokens)
|
|
print untokenize(back)
|