Start process of making python3 compatible

This commit is contained in:
rocky
2015-12-11 13:44:23 -05:00
parent 365cdfc941
commit d3c732298c
25 changed files with 282 additions and 391 deletions

15
.travis.yml Normal file
View File

@@ -0,0 +1,15 @@
language: python
sudo: false
python:
- '2.7'
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- pip install .
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi
script:
- make test

95
Makefile Normal file
View File

@@ -0,0 +1,95 @@
# Compatibility for us old-timers.
# Note: This makefile include remake-style target comments.
# These comments before the targets start with #:
# remake --tasks to shows the targets and the comments
GIT2CL ?= git2cl
PYTHON ?= python
PYTHON3 ?= python3
RM ?= rm
LINT = flake8
#EXTRA_DIST=ipython/ipy_trepan.py trepan
PHONY=check clean dist distclean test test-unit test-functional rmChangeLog clean_pyc nosetests
#: Default target - same as "check"
all: check
#: Make HTML docs
html:
cd docs && $(MAKE) html
#: Same as "check"
test: check
#: Same as "check"
nosetests: check
#: Run all tests
check-short: test-unit-short
#: Run all tests: unit, functional and integration verbosely
check: test-unit lint
#: Run unit (white-box) tests
test-unit:
$(PYTHON) ./setup.py nosetests
#: Run unit (white-box) tests
test-unit-short:
$(PYTHON) ./setup.py nosetests --quiet 2>&1 | \
$(PYTHON) ./test/make-check-filter.py
#: Clean up temporary files and .pyc files
clean: clean_pyc
$(PYTHON) ./setup.py $@
#: Create source (tarball) and binary (egg) distribution
dist:
$(PYTHON) ./setup.py sdist bdist_egg
#: Remove .pyc files
clean_pyc:
$(RM) -f */*.pyc */*/*.pyc */*/*/*.pyc */*/*/*/*.pyc
#: Create source tarball
sdist:
$(PYTHON) ./setup.py sdist
#: Style check. Set env var LINT to pyflakes, flake, or flake8
lint:
$(LINT) trepan_deparse/deparser.py
#: Create binary egg distribution
bdist_egg:
$(PYTHON) ./setup.py bdist_egg
# It is too much work to figure out how to add a new command to distutils
# to do the following. I'm sure distutils will someday get there.
DISTCLEAN_FILES = build dist *.pyc
#: Remove ALL derived files
distclean: clean
-rm -fr $(DISTCLEAN_FILES) || true
-find . -name \*.pyc -exec rm -v {} \;
-find . -name \*.egg-info -exec rm -vr {} \;
#: Install package locally
verbose-install:
$(PYTHON) ./setup.py install
#: Install package locally without the verbiage
install:
$(PYTHON) ./setup.py install >/dev/null
rmChangeLog:
rm ChangeLog || true
#: Create a ChangeLog from git via git log and git2cl
ChangeLog: rmChangeLog
git log --pretty --numstat --summary | $(GIT2CL) >$@
.PHONY: $(PHONY)

View File

@@ -4,12 +4,12 @@
# Copyright (c) 2000-2002 by hartmut Goebel <h.goebel@crazy-compilers.com>
#
'''
Usage: uncompyle2 [OPTIONS]... [ FILE | DIR]...
Usage: uncompyle6 [OPTIONS]... [ FILE | DIR]...
Examples:
uncompyle2 foo.pyc bar.pyc # decompile foo.pyc, bar.pyc to stdout
uncompyle2 -o . foo.pyc bar.pyc # decompile to ./foo.pyc_dis and ./bar.pyc_dis
uncompyle2 -o /tmp /usr/lib/python1.5 # decompile whole library
uncompyle6 foo.pyc bar.pyc # decompile foo.pyc, bar.pyc to stdout
uncompyle6 -o . foo.pyc bar.pyc # decompile to ./foo.pyc_dis and ./bar.pyc_dis
uncompyle6 -o /tmp /usr/lib/python1.5 # decompile whole library
Options:
-o <path> output decompiled files to this path:
@@ -41,15 +41,15 @@ Extensions of generated files:
'''
Usage_short = \
"uncompyle2 [--help] [--verify] [--showasm] [--showast] [-o <path>] FILE|DIR..."
"uncompyle6 [--help] [--verify] [--showasm] [--showast] [-o <path>] FILE|DIR..."
import sys, os, getopt
import os.path
from uncompyle2 import main, verify
from uncompyle6 import main, verify
import time
if sys.version[:3] != '2.7':
print >>sys.stderr, 'Error: uncompyle2 requires Python 2.7.'
print >>sys.stderr, 'Error: uncompyle6 requires Python 2.7.'
sys.exit(-1)
showasm = showast = do_verify = numproc = recurse_dirs = 0

View File

@@ -4,12 +4,12 @@
from distutils.core import setup, Extension
setup (name = "uncompyle2",
version = "1.1",
setup (name = "uncompyle6",
version = "2.0",
description = "Python byte-code to source-code converter",
author = "Mysterie",
author_email = "kajusska@gmail.com",
url = "http://github.com/Mysterie/uncompyle2",
packages=['uncompyle2', 'uncompyle2.opcode'],
scripts=['scripts/uncompyle2']
packages=['uncompyle6', 'uncompyle6.opcode'],
scripts=['scripts/uncompyle6']
)

18
test_one Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
PAGER=${PAGER:-less}
file=$1
shift
options=$@
#BASEDIR=test/bytecode_1.5
#BASEDIR=test/bytecode_2.0
#BASEDIR=test/bytecode_2.1
#BASEDIR=test/bytecode_2.2
BASEDIR=test/bytecode_2.7
if [[ `dirname $file` == '.' ]] ; then
file=$BASEDIR/test_$file.pyc
fi
python -u ./scripts/uncompyle6 $options $file 2>&1 | $PAGER

19
tox.ini Normal file
View File

@@ -0,0 +1,19 @@
; Settings file for flake8:
; http://flake8.readthedocs.org/en/latest/config.html#settings
[flake8]
exclude = .tox,./build,./trepan/processor/command/tmp
filename = *.py
ignore = C901,E113,E121,E122,E123,E125,E126,E127,E128,E129,E201,E202,E203,E221,E222,E225,E226,E241,E242,E251,E261,E271,E272,E302,E401,E501,F401,E701,E702
[tox]
envlist = py26, py27, pypy
[testenv]
deps =
requests>=0.8.8
mock>=1.0.1
commands = python -W always setup.py nosetests {posargs}
[testenv:py27]
deps =
flake8

View File

@@ -3,6 +3,9 @@ from uncompyle2 import uncompyle, walker, verify, magics
from uncompyle2.spark import GenericASTTraversal, GenericASTTraversalPruningException
import sys, inspect, types, cStringIO
from collections import namedtuple
NodeInfo = namedtuple("NodeInfo", "node start finish")
class FindWalker(walker.Walker, object):
stacked_params = ('f', 'indent', 'isLambda', '_globals')
@@ -22,6 +25,10 @@ class FindWalker(walker.Walker, object):
self.currentclass = None
self.pending_newlines = 0
self.found_offset = False
self.offsets = {}
f = property(lambda s: s.__params['f'],
lambda s, x: s.__params.__setitem__('f', x),
lambda s: s.__params.__delitem__('f'),
@@ -47,10 +54,10 @@ class FindWalker(walker.Walker, object):
node = self.ast
if hasattr(node, 'offset'):
print "Name %s has an offset %d" % (self.typestring(node), node.offset)
start = len(self.f.getvalue())
if node.offset == self.find_offset:
self.found_offset = True
print 'BINGO!'
# print 'BINGO!'
try:
name = 'n_' + self.typestring(node)
@@ -60,6 +67,12 @@ class FindWalker(walker.Walker, object):
else:
self.default(node)
except GenericASTTraversalPruningException:
if hasattr(node, 'offset'):
self.offsets[node.offset] = NodeInfo(node = node,
start = start,
finish = len(self.f.getvalue()))
# print self.offsets[node.offset]
# print self.f.getvalue()[start:]
return
for kid in node:
@@ -70,6 +83,9 @@ class FindWalker(walker.Walker, object):
func = getattr(self, name)
func(node)
return
def find_source(self, offset, ast, customize, isLambda=0, returnNone=False):
"""convert AST to source code"""
@@ -86,10 +102,11 @@ class FindWalker(walker.Walker, object):
self.print_(self.indent, 'pass')
else:
self.customize(customize)
result = self.traverse(ast, isLambda=isLambda)
if isLambda:
self.write(self.traverse(ast, isLambda=isLambda))
self.write(result)
else:
self.print_(self.traverse(ast, isLambda=isLambda))
self.print_(result)
self.return_none = rn
# FIXME; below duplicated the code, since we don't find self.__params
@@ -172,7 +189,7 @@ def uncompyle_test():
uncompyle(2.7, co, sys.stdout, 1)
print
print '------------------------'
uncompyle_find(2.7, co, 24)
uncompyle_find(2.7, co, 33)
finally:
del frame

View File

@@ -1,86 +0,0 @@
import struct
__all__ = ['magics', 'versions']
def __build_magic(magic):
return struct.pack('Hcc', magic, '\r', '\n')
def __by_version(magics):
by_version = {}
for m, v in magics.items():
by_version[v] = m
return by_version
versions = {
# taken from from Python/import.c
# magic, version
__build_magic(20121): '1.5', #1.5, 1.5.1, 1.5.2
__build_magic(50428): '1.6', #1.6
__build_magic(50823): '2.0', #2.0, 2.0.1
__build_magic(60202): '2.1', #2.1, 2.1.1, 2.1.2
__build_magic(60717): '2.2', #2.2
__build_magic(62011): '2.3', #2.3a0
__build_magic(62021): '2.3', #2.3a0
__build_magic(62041): '2.4', #2.4a0
__build_magic(62051): '2.4', #2.4a3
__build_magic(62061): '2.4', #2.4b1
__build_magic(62071): '2.5', #2.5a0
__build_magic(62081): '2.5', #2.5a0 (ast-branch)
__build_magic(62091): '2.5', #2.5a0 (with)
__build_magic(62092): '2.5', #2.5a0 (changed WITH_CLEANUP opcode)
__build_magic(62101): '2.5', #2.5b3 (fix wrong code: for x, in ...)
__build_magic(62111): '2.5', #2.5b3 (fix wrong code: x += yield)
__build_magic(62121): '2.5', #2.5c1 (fix wrong lnotab with for loops and
# storing constants that should have been removed
__build_magic(62131): '2.5', #2.5c2 (fix wrong code: for x, in ... in listcomp/genexp)
__build_magic(62151): '2.6', #2.6a0 (peephole optimizations & STORE_MAP)
__build_magic(62161): '2.6', #2.6a1 (WITH_CLEANUP optimization)
__build_magic(62171): '2.7', #2.7a0 (optimize list comprehensions/change LIST_APPEND)
__build_magic(62181): '2.7', #2.7a0 (optimize conditional branches:
# introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE)
__build_magic(62191): '2.7', #2.7a0 (introduce SETUP_WITH)
__build_magic(62201): '2.7', #2.7a0 (introduce BUILD_SET)
__build_magic(62211): '2.7', #2.7a0 (introduce MAP_ADD and SET_ADD)
__build_magic(3000): '3.0', #3.000
__build_magic(3010): '3.0', #3.000 (removed UNARY_CONVERT)
__build_magic(3020): '3.0', #3.000 (added BUILD_SET)
__build_magic(3030): '3.0', #3.000 (added keyword-only parameters)
__build_magic(3040): '3.0', #3.000 (added signature annotations)
__build_magic(3050): '3.0', #3.000 (print becomes a function)
__build_magic(3060): '3.0', #3.000 (PEP 3115 metaclass syntax)
__build_magic(3061): '3.0', #3.000 (string literals become unicode)
__build_magic(3071): '3.0', #3.000 (PEP 3109 raise changes)
__build_magic(3081): '3.0', #3.000 (PEP 3137 make __file__ and __name__ unicode)
__build_magic(3091): '3.0', #3.000 (kill str8 interning)
__build_magic(3101): '3.0', #3.000 (merge from 2.6a0, see 62151)
__build_magic(3103): '3.0', #3.000 (__file__ points to source file)
__build_magic(3111): '3.0', #3.0a4 (WITH_CLEANUP optimization).
__build_magic(3131): '3.0', #3.0a5 (lexical exception stacking, including POP_EXCEPT)
__build_magic(3141): '3.1', #3.1a0 (optimize list, set and dict comprehensions)
__build_magic(3151): '3.1', #3.1a0 (optimize conditional branches)
__build_magic(3160): '3.2', #3.2a0 (add SETUP_WITH)
__build_magic(3170): '3.2', #3.2a1 (add DUP_TOP_TWO, remove DUP_TOPX and ROT_FOUR)
__build_magic(3180): '3.2', #3.2a2 (add DELETE_DEREF)
}
magics = __by_version(versions)
def __show(text, magic):
print text, struct.unpack('BBBB', magic), \
struct.unpack('HBB', magic)
def test():
import imp
magic_20 = by_version['2.0']
current = imp.get_magic()
current_version = magics[current]
magic_current = by_version[ current_version ]
print type(magic_20), len(magic_20), repr(magic_20)
print
print 'This Python interpreter has version', current_version
__show('imp.get_magic():\t', current),
__show('magic[current_version]:\t', magic_current)
__show('magic_20:\t\t', magic_20)
if __name__ == '__main__':
test()

View File

@@ -1,277 +0,0 @@
# Copyright (c) 1999 John Aycock
# Copyright (c) 2000-2002 by hartmut Goebel <h.goebel@crazy-compilers.com>
# Copyright (c) 2005 by Dan Pascu <dan@windowmaker.org>
#
# See main module for license.
#
__all__ = ['Token', 'Scanner', 'Code']
import types
from collections import namedtuple
from array import array
from operator import itemgetter
from uncompyle2.opcode import opcode_25, opcode_26, opcode_27
class Token:
'''
Class representing a byte-code token.
A byte-code token is equivalent to the contents of one line
as output by dis.dis().
'''
def __init__(self, type_, attr=None, pattr=None, offset=-1, linestart=False):
self.type = intern(type_)
self.attr = attr
self.pattr = pattr
self.offset = offset
self.linestart = linestart
def __cmp__(self, o):
if isinstance(o, Token):
# both are tokens: compare type and pattr
return cmp(self.type, o.type) or cmp(self.pattr, o.pattr)
else:
return cmp(self.type, o)
def __repr__(self): return str(self.type)
def __str__(self):
pattr = self.pattr
if self.linestart:
return '\n%s\t%-17s %r' % (self.offset, self.type, pattr)
else:
return '%s\t%-17s %r' % (self.offset, self.type, pattr)
def __hash__(self): return hash(self.type)
def __getitem__(self, i): raise IndexError
class Code:
'''
Class for representing code-objects.
This is similar to the original code object, but additionally
the diassembled code is stored in the attribute '_tokens'.
'''
def __init__(self, co, scanner, classname=None):
for i in dir(co):
if i.startswith('co_'):
setattr(self, i, getattr(co, i))
self._tokens, self._customize = scanner.disassemble(co, classname)
class Scanner(object):
opc = None # opcode module
def __init__(self, version):
if version == 2.7:
self.opc = opcode_27
elif version == 2.6:
self.opc = opcode_26
elif version == 2.5:
self.opc = opcode_25
return self.resetTokenClass()
def setShowAsm(self, showasm, out=None):
self.showasm = showasm
self.out = out
def setTokenClass(self, tokenClass):
assert type(tokenClass) == types.ClassType
self.Token = tokenClass
return self.Token
def resetTokenClass(self):
return self.setTokenClass(Token)
def get_target(self, pos, op=None):
if op is None:
op = self.code[pos]
target = self.get_argument(pos)
if op in self.opc.hasjrel:
target += pos + 3
return target
def get_argument(self, pos):
arg = self.code[pos+1] + self.code[pos+2] * 256
return arg
def print_bytecode(self):
for i in self.op_range(0, len(self.code)):
op = self.code[i]
if op in self.opc.hasjabs+self.opc.hasjrel:
dest = self.get_target(i, op)
print '%i\t%s\t%i' % (i, self.opc.opname[op], dest)
else:
print '%i\t%s\t' % (i, self.opc.opname[op])
def first_instr(self, start, end, instr, target=None, exact=True):
'''
Find the first <instr> in the block from start to end.
<instr> is any python bytecode instruction or a list of opcodes
If <instr> is an opcode with a target (like a jump), a target
destination can be specified which must match precisely if exact
is True, or if exact is False, the instruction which has a target
closest to <target> will be returned.
Return index to it or None if not found.
'''
code = self.code
assert(start>=0 and end<=len(code))
try: None in instr
except: instr = [instr]
pos = None
distance = len(code)
for i in self.op_range(start, end):
op = code[i]
if op in instr:
if target is None:
return i
dest = self.get_target(i, op)
if dest == target:
return i
elif not exact:
_distance = abs(target - dest)
if _distance < distance:
distance = _distance
pos = i
return pos
def last_instr(self, start, end, instr, target=None, exact=True):
'''
Find the last <instr> in the block from start to end.
<instr> is any python bytecode instruction or a list of opcodes
If <instr> is an opcode with a target (like a jump), a target
destination can be specified which must match precisely if exact
is True, or if exact is False, the instruction which has a target
closest to <target> will be returned.
Return index to it or None if not found.
'''
code = self.code
if not (start>=0 and end<=len(code)):
return None
try: None in instr
except: instr = [instr]
pos = None
distance = len(code)
for i in self.op_range(start, end):
op = code[i]
if op in instr:
if target is None:
pos = i
else:
dest = self.get_target(i, op)
if dest == target:
distance = 0
pos = i
elif not exact:
_distance = abs(target - dest)
if _distance <= distance:
distance = _distance
pos = i
return pos
def all_instr(self, start, end, instr, target=None, include_beyond_target=False):
'''
Find all <instr> in the block from start to end.
<instr> is any python bytecode instruction or a list of opcodes
If <instr> is an opcode with a target (like a jump), a target
destination can be specified which must match precisely.
Return a list with indexes to them or [] if none found.
'''
code = self.code
assert(start>=0 and end<=len(code))
try: None in instr
except: instr = [instr]
result = []
for i in self.op_range(start, end):
op = code[i]
if op in instr:
if target is None:
result.append(i)
else:
t = self.get_target(i, op)
if include_beyond_target and t >= target:
result.append(i)
elif t == target:
result.append(i)
return result
def op_size(self, op):
if op < self.opc.HAVE_ARGUMENT and op not in self.opc.hasArgumentExtended:
return 1
else:
return 3
def op_hasArgument(self, op):
return self.op_size(op) > 1
def op_range(self, start, end):
while start < end:
yield start
start += self.op_size(self.code[start])
def remove_mid_line_ifs(self, ifs):
filtered = []
for i in ifs:
if self.lines[i].l_no == self.lines[i+3].l_no:
if self.code[self.prev[self.lines[i].next]] in (self.opc.PJIT, self.opc.PJIF):
continue
filtered.append(i)
return filtered
def rem_or(self, start, end, instr, target=None, include_beyond_target=False):
'''
Find all <instr> in the block from start to end.
<instr> is any python bytecode instruction or a list of opcodes
If <instr> is an opcode with a target (like a jump), a target
destination can be specified which must match precisely.
Return a list with indexes to them or [] if none found.
'''
code = self.code
assert(start>=0 and end<=len(code))
try: None in instr
except: instr = [instr]
result = []
for i in self.op_range(start, end):
op = code[i]
if op in instr:
if target is None:
result.append(i)
else:
t = self.get_target(i, op)
if include_beyond_target and t >= target:
result.append(i)
elif t == target:
result.append(i)
pjits = self.all_instr(start, end, self.opc.PJIT)
filtered = []
for pjit in pjits:
tgt = self.get_target(pjit)-3
for i in result:
if i <= pjit or i >= tgt:
filtered.append(i)
result = filtered
filtered = []
return result
def restrict_to_parent(self, target, parent):
'''Restrict pos to parent boundaries.'''
if not (parent['start'] < target < parent['end']):
target = parent['end']
return target

88
uncompyle6/magics.py Executable file
View File

@@ -0,0 +1,88 @@
from __future__ import print_function
import struct
__all__ = ['magics', 'versions']
def __build_magic(magic):
return struct.pack('Hcc', magic, '\r', '\n')
by_magic = {}
by_version = {}
def __by_version(magics):
for m, v in magics.items():
by_magic[m] = v
by_version[v] = m
return by_version
versions = {
# taken from from Python/import.c
# magic, version
__build_magic(20121): '1.5', # 1.5, 1.5.1, 1.5.2
__build_magic(50428): '1.6', # 1.6
__build_magic(50823): '2.0', # 2.0, 2.0.1
__build_magic(60202): '2.1', # 2.1, 2.1.1, 2.1.2
__build_magic(60717): '2.2', # 2.2
__build_magic(62011): '2.3', # 2.3a0
__build_magic(62021): '2.3', # 2.3a0
__build_magic(62041): '2.4', # 2.4a0
__build_magic(62051): '2.4', # 2.4a3
__build_magic(62061): '2.4', # 2.4b1
__build_magic(62071): '2.5', # 2.5a0
__build_magic(62081): '2.5', # 2.5a0 (ast-branch)
__build_magic(62091): '2.5', # 2.5a0 (with)
__build_magic(62092): '2.5', # 2.5a0 (changed WITH_CLEANUP opcode)
__build_magic(62101): '2.5', # 2.5b3 (fix wrong code: for x, in ...)
__build_magic(62111): '2.5', # 2.5b3 (fix wrong code: x += yield)
__build_magic(62121): '2.5', # 2.5c1 (fix wrong lnotab with for loops and
# storing constants that should have been removed
__build_magic(62131): '2.5', # 2.5c2 (fix wrong code: for x, in ... in listcomp/genexp)
__build_magic(62151): '2.6', # 2.6a0 (peephole optimizations & STORE_MAP)
__build_magic(62161): '2.6', # 2.6a1 (WITH_CLEANUP optimization)
__build_magic(62171): '2.7', # 2.7a0 (optimize list comprehensions/change LIST_APPEND)
__build_magic(62181): '2.7', # 2.7a0 (optimize conditional branches:
# introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE)
__build_magic(62191): '2.7', # 2.7a0 (introduce SETUP_WITH)
__build_magic(62201): '2.7', # 2.7a0 (introduce BUILD_SET)
__build_magic(62211): '2.7', # 2.7a0 (introduce MAP_ADD and SET_ADD)
__build_magic(3000): '3.0', # 3.000
__build_magic(3010): '3.0', # 3.000 (removed UNARY_CONVERT)
__build_magic(3020): '3.0', # 3.000 (added BUILD_SET)
__build_magic(3030): '3.0', # 3.000 (added keyword-only parameters)
__build_magic(3040): '3.0', # 3.000 (added signature annotations)
__build_magic(3050): '3.0', # 3.000 (print becomes a function)
__build_magic(3060): '3.0', # 3.000 (PEP 3115 metaclass syntax)
__build_magic(3061): '3.0', # 3.000 (string literals become unicode)
__build_magic(3071): '3.0', # 3.000 (PEP 3109 raise changes)
__build_magic(3081): '3.0', # 3.000 (PEP 3137 make __file__ and __name__ unicode)
__build_magic(3091): '3.0', # 3.000 (kill str8 interning)
__build_magic(3101): '3.0', # 3.000 (merge from 2.6a0, see 62151)
__build_magic(3103): '3.0', # 3.000 (__file__ points to source file)
__build_magic(3111): '3.0', # 3.0a4 (WITH_CLEANUP optimization).
__build_magic(3131): '3.0', # 3.0a5 (lexical exception stacking, including POP_EXCEPT)
__build_magic(3141): '3.1', # 3.1a0 (optimize list, set and dict comprehensions)
__build_magic(3151): '3.1', # 3.1a0 (optimize conditional branches)
__build_magic(3160): '3.2', # 3.2a0 (add SETUP_WITH)
__build_magic(3170): '3.2', # 3.2a1 (add DUP_TOP_TWO, remove DUP_TOPX and ROT_FOUR)
__build_magic(3180): '3.2', # 3.2a2 (add DELETE_DEREF)
}
magics = __by_version(versions)
def __show(text, magic):
print(text, struct.unpack('BBBB', magic), struct.unpack('HBB', magic))
def test():
import imp
magic_20 = magics['2.0']
current = imp.get_magic()
current_version = struct.unpack('HBB', current)[0]
magic_current = by_magic[ current ]
print(type(magic_20), len(magic_20), repr(magic_20))
print()
print('This Python interpreter has version', magic_current)
print('Magic code: ', current_version)
print(type(magic_20), len(magic_20), repr(magic_20))
if __name__ == '__main__':
test()

View File

@@ -26,12 +26,14 @@
%c evaluate N[A] recursively*
%C evaluate N[A[0]]..N[A[1]-1] recursively, separate by A[2]*
%P same as %C but sets operator precedence
%, print ',' if last %C only printed one item (for tuples--unused)
%| tab to current indentation level
%+ increase current indentation level
%- decrease current indentation level
%{...} evaluate ... in context of N
%% literal '%'
%p evaluate N setting precedence
* indicates an argument (A) required.