diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2130e0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.class +*.egg-info +*.pyc diff --git a/bin/j2py b/bin/j2py index 16e6ac3..6eb1a40 100755 --- a/bin/j2py +++ b/bin/j2py @@ -18,7 +18,7 @@ from java2python.config import Config from java2python.lib import escapes -version = '0.5.0' +version = '0.5.1' def logLevel(value): @@ -68,6 +68,7 @@ def runOneOrMany(options): for name in [name for name in files if name.endswith('.java')]: fullname = path.join(dirname, name) options.inputfile = fullname + info('opening %s', fullname) if outfile and outfile != '-' and not isinstance(outfile, file): full = path.abspath(path.join(outfile, fullname)) head, tail = path.split(full) diff --git a/doc/install.md b/doc/install.md index aa66c79..bd2cf6e 100644 --- a/doc/install.md +++ b/doc/install.md @@ -12,7 +12,7 @@ Kids these days have it easy: We need the ANTLR Python runtime before we can install java2python: - # wget http://www.antlr.org/download/antlr-3.1.3.tar.gz + # wget http://www.antlr3.org/download/antlr-3.1.3.tar.gz # tar xfz antlr-3.1.3.tar.gz # cd antlr-3.1.3/runtime/Python/ # python setup.py install @@ -21,8 +21,8 @@ We need the ANTLR Python runtime before we can install java2python: Now the goodness: - # wget https://github.com/downloads/natural/java2python/java2python-0.5.0.tar.gz - # tar xfz java2python-0.5.0.tar.gz + # wget https://github.com/downloads/natural/java2python/java2python-0.5.1.tar.gz + # tar xfz java2python-0.5.1.tar.gz # cd java2python # python setup.py install @@ -45,7 +45,6 @@ The development dependencies (what you need if you're coding java2python) are [ANTLR][], also version 3.1.3, GNU make, and a JVM. -[version 3.1.3 of the Python runtime]: http://www.antlr.org/download/antlr-3.1.3.tar.gz +[version 3.1.3 of the Python runtime]: http://www.antlr3.org/download/antlr-3.1.3.tar.gz [Python runtime]: http://www.antlr.org/wiki/display/ANTLR3/Python+runtime [ANTLR]: http://www.antlr.org - diff --git a/doc/readme.md b/doc/readme.md index 71f67c6..6629996 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -24,8 +24,14 @@ The [Usage][] page explains how to run the `j2py` script. The [Customization][] doc describes how to customize translation behavior. +#### Tests + +The [Tests][] page documents the test suite. + + [Customization]: https://github.com/natural/java2python/tree/master/doc/customization.md [Installation]: https://github.com/natural/java2python/tree/master/doc/install.md [Introduction]: https://github.com/natural/java2python/tree/master/doc/intro.md [Translation Details]: https://github.com/natural/java2python/tree/master/doc/translation.md [Usage]: https://github.com/natural/java2python/tree/master/doc/usage.md +[Tests]: https://github.com/natural/java2python/tree/master/doc/tests.md diff --git a/doc/tests.md b/doc/tests.md new file mode 100644 index 0000000..4cb31e8 --- /dev/null +++ b/doc/tests.md @@ -0,0 +1,85 @@ +## Tests + +The java2python package includes a [test suite][] for exercising the compiler and +its various translation features. This doc explains how the tests work, how to +run these suite, and how to add new tests to it. + +### How the Test Suite Works + +The test suite is a [makefile][] that finds `.java` files in the same directory, +converts each to Python, runs both programs, and then compares their output. If +the output matches, the test is considered successful. If not, it's considered +a failure. + +### How to Run the Test Suite + +The simplest way to run the suite is to run all of it: + +```bash +$ cd some_path_to/java2python/test +$ make +``` + +This will print lines like this: + +``` +... +[PASS] Class00 +[PASS] Class01 +[PASS] Class02 +... +``` + +You can also run an individual test like this: + +```bash +$ make Class02 +[PASS] Class02 +``` + +Notice that there isn't a suffix to the file name; you don't run `make +Class02.java`, just `make Class02`. If you supply an extension, nothing will +happen and the test won't run. + +The `test` directory contains two helper scripts that you can use during +development. The first is [runjava][], which runs the Java compiler and the +Java VM with the indicated file. Use it like this: + +```bash +$ ./runjava Class01.java +Hello, world. +``` + +The second script is [runj2py][], which is a handy shortcut for running the +`j2py` script with preset command line arguments for the test configuration. +You run it like this: + +```bash +$ ./runj2py Class01.java +#!/usr/bin/env python +""" generated source for module Class01 """ +class Class01(object): +... +``` + +### Adding New Tests + +When a new compiler feature is added, or when the translation semantics change, +it's a good idea to add one or more tests to the test suite. Follow this +general outline: + +1. Create a Java source file that exhibits the language feature in question. + +2. Name the Java source file `FeatureNN` where `NN` is the next number in +sequence for `Feature`, e.g., `Class14.java`. + +3. In your Java source, write one or more values to stdout with +`System.out.println`. + +4. Check the comparison via `make FeatureNN`. If the test passes, it might +indicate the new feature is working correctly. + +[test suite]: https://github.com/natural/java2python/tree/master/test/ +[makefile]: https://github.com/natural/java2python/blob/master/test/Makefile +[runjava]: https://github.com/natural/java2python/blob/master/test/runjava +[runj2py]: https://github.com/natural/java2python/blob/master/test/runj2py diff --git a/doc/translation.md b/doc/translation.md index 56e7036..41c7a6e 100644 --- a/doc/translation.md +++ b/doc/translation.md @@ -52,7 +52,7 @@ necessary, those expressions are moved outside of statements. All of the following assignment operators are translated into their Python equivalents: - + = += -= *= /= &= |= ^= %= <<= >>= The bit shift right (`>>>`)and bit shift assign right (`>>>=`) operators are @@ -131,7 +131,7 @@ statements. Java `try` and `catch` statements are translated to equivalent Python `try` and `except` statements. - + #### switch and case Java `switch` and `case` statements are translated to equivalent Python `if` @@ -160,7 +160,7 @@ statements. #### throw -Java `throw` statements are translated to equivalent Python `throw` statements. +Java `throw` statements are translated to equivalent Python `raise` statements. #### break diff --git a/java2python/compiler/template.py b/java2python/compiler/template.py index e117df3..4f4dfe1 100644 --- a/java2python/compiler/template.py +++ b/java2python/compiler/template.py @@ -119,6 +119,7 @@ def __init__(self, config, name=None, type=None, parent=None): self.children = [] self.config = config self.decorators = [] + self.overloaded = None self.factory = Factory(config) self.modifiers = [] self.name = name @@ -172,7 +173,7 @@ def configHandler(self, part, suffix='Handler', default=None): def configHandlers(self, part, suffix='Handlers'): """ Returns config handlers for this type of template """ name = '{0}{1}{2}'.format(self.typeName, part, suffix) - return imap(self.toIter, self.config.last(name, ())) + return imap(self.toIter, chain(*self.config.every(name, []))) def dump(self, fd, level=0): """ Writes the Python source code for this template to the given file. """ diff --git a/java2python/compiler/visitor.py b/java2python/compiler/visitor.py index 40b6ea7..f62e53e 100644 --- a/java2python/compiler/visitor.py +++ b/java2python/compiler/visitor.py @@ -46,7 +46,7 @@ def insertComments(self, tmpl, tree, index, memo): """ Add comments to the template from tokens in the tree. """ prefix = self.config.last('commentPrefix', '# ') cache, parser, comTypes = memo.comments, tree.parser, tokens.commentTypes - comNew = lambda t:t.type in comTypes and t.index not in cache + comNew = lambda t:t.type in comTypes and (t.index not in cache) for tok in ifilter(comNew, parser.input.tokens[memo.last:index]): cache.add(tok.index) @@ -129,7 +129,12 @@ def acceptType(self, node, memo): acceptAt = makeAcceptType('at') acceptClass = makeAcceptType('klass') acceptEnum = makeAcceptType('enum') - acceptInterface = makeAcceptType('interface') + _acceptInterface = makeAcceptType('interface') + + def acceptInterface(self, node, memo): + module = self.parents(lambda x:x.isModule).next() + module.needsAbstractHelpers = True + return self._acceptInterface(node, memo) class Module(TypeAcceptor, Base): @@ -173,7 +178,7 @@ def nodesToAnnos(self, branch, memo): if defKey: deco = self.factory.expr(left=name, fs='@{left}({right})') deco.right = right = self.factory.expr(parent=deco) - right.walk(defKey.firstChild()) + right.walk(defKey.firstChild(), memo) else: deco = self.factory.expr(left=name, fs='@{left}({right})') arg = deco.right = self.factory.expr(parent=deco) @@ -228,7 +233,10 @@ def acceptVarDeclaration(self, node, memo): if node.firstChildOfType(tokens.TYPE).firstChildOfType(tokens.ARRAY_DECLARATOR_LIST): val = assgnExp.pushRight('[]') else: - val = assgnExp.pushRight('{0}()'.format(identExp.type)) + if node.firstChildOfType(tokens.TYPE).firstChild().type != tokens.QUALIFIED_TYPE_IDENT: + val = assgnExp.pushRight('{0}()'.format(identExp.type)) + else: + val = assgnExp.pushRight('None') return self @@ -358,7 +366,7 @@ class Interface(Class): """ Interface -> accepts AST branches for Java interfaces. """ -class MethodContent(Base): +class MethodContent(VarAcceptor, Base): """ MethodContent -> accepts trees for blocks within methods. """ def acceptAssert(self, node, memo): @@ -399,6 +407,10 @@ def acceptCatch(self, node, memo): def acceptContinue(self, node, memo): """ Accept and process a continue statement. """ + parent = node.parents(lambda x: x.type in {tokens.FOR, tokens.FOR_EACH, tokens.DO, tokens.WHILE}).next() + if parent.type == tokens.FOR: + updateStat = self.factory.expr(parent=self) + updateStat.walk(parent.firstChildOfType(tokens.FOR_UPDATE), memo) contStat = self.factory.statement('continue', fs=FS.lsr, parent=self) if len(node.children): warn('Detected unhandled continue statement with label; generated code incorrect.') @@ -432,17 +444,20 @@ def acceptExpr(self, node, memo): def acceptFor(self, node, memo): """ Accept and process a 'for' statement. """ - self.walk(node.firstChildOfType(tokens.FOR_INIT)) + self.walk(node.firstChildOfType(tokens.FOR_INIT), memo) whileStat = self.factory.statement('while', fs=FS.lsrc, parent=self) cond = node.firstChildOfType(tokens.FOR_CONDITION) if not cond.children: whileStat.expr.right = 'True' else: - whileStat.expr.walk(cond) + whileStat.expr.walk(cond, memo) whileBlock = self.factory.methodContent(parent=self) - whileBlock.walk(node.firstChildOfType(tokens.BLOCK_SCOPE)) + if not node.firstChildOfType(tokens.BLOCK_SCOPE).children: + self.factory.expr(left='pass', parent=whileBlock) + else: + whileBlock.walk(node.firstChildOfType(tokens.BLOCK_SCOPE), memo) updateStat = self.factory.expr(parent=whileBlock) - updateStat.walk(node.firstChildOfType(tokens.FOR_UPDATE)) + updateStat.walk(node.firstChildOfType(tokens.FOR_UPDATE), memo) def acceptForEach(self, node, memo): """ Accept and process a 'for each' style statement. """ @@ -479,16 +494,24 @@ def acceptIf(self, node, memo): else: nextBlock = self.factory.methodContent(parent=self) nextBlock.walk(nextNode.children[1], memo) - nextNode = nextNode.children[2] - nextType = nextNode.type + + try: + nextNode = nextNode.children[2] + except (IndexError, ): + nextType = None + else: + nextType = nextNode.type if nextType == tokens.EXPR: elseStat = self.factory.statement('else', fs=FS.lc, parent=self) elseBlock = self.factory.expr(parent=elseStat) elseBlock.walk(nextNode, memo) - else: # nextType != tokens.BLOCK_SCOPE: - self.factory.statement('else', fs=FS.lc, parent=self) - self.factory.methodContent(parent=self).walk(nextNode, memo) + elif nextType: # nextType != tokens.BLOCK_SCOPE: + elseStat = self.factory.statement('else', fs=FS.lc, parent=self) + if nextNode.children: + self.factory.methodContent(parent=self).walk(nextNode, memo) + else: + self.factory.expr(left='pass', parent=elseStat) def acceptSwitch(self, node, memo): @@ -505,8 +528,8 @@ def acceptSwitch(self, node, memo): return # we have at least one node... parExpr = self.factory.expr(parent=self) - parExpr.walk(parNode) - eqFs = FS.l + '==' + FS.r + parExpr.walk(parNode, memo) + eqFs = FS.l + ' == ' + FS.r for caseIdx, caseNode in enumerate(caseNodes): isDefault, isFirst = caseNode.type==tokens.DEFAULT, caseIdx==0 @@ -519,7 +542,7 @@ def acceptSwitch(self, node, memo): if not isDefault: right = self.factory.expr(parent=parExpr) - right.walk(caseNode.firstChildOfType(tokens.EXPR)) + right.walk(caseNode.firstChildOfType(tokens.EXPR), memo) caseExpr.expr.right = self.factory.expr(left=parExpr, right=right, fs=eqFs) caseContent = self.factory.methodContent(parent=self) for child in caseNode.children[1:]: @@ -602,7 +625,7 @@ def acceptWhile(self, node, memo): whileStat.walk(blkNode, memo) -class Method(VarAcceptor, ModifiersAcceptor, MethodContent): +class Method(ModifiersAcceptor, MethodContent): """ Method -> accepts AST branches for method-level objects. """ def acceptFormalParamStdDecl(self, node, memo): @@ -641,7 +664,7 @@ def nodeOpExpr(self, node, memo): """ Accept and processes an operator expression. """ factory = self.factory.expr self.fs = FS.l + ' ' + node.text + ' ' + FS.r - self.left, self.right = visitors = factory(parent=self), factory() + self.left, self.right = visitors = factory(parent=self), factory(parent=self) self.zipWalk(node.children, visitors, memo) acceptAnd = nodeOpExpr @@ -678,7 +701,6 @@ def acceptPreformatted(self, node, memo): return acceptPreformatted acceptArrayElementAccess = makeNodePreformattedExpr(FS.l + '[' + FS.r + ']') - acceptCastExpr = makeNodePreformattedExpr(FS.l + '(' + FS.r + ')' ) # problem? acceptDiv = makeNodePreformattedExpr(FS.l + ' / ' + FS.r) acceptLogicalAnd = makeNodePreformattedExpr(FS.l + ' and ' + FS.r) acceptLogicalNot = makeNodePreformattedExpr('not ' + FS.l) @@ -690,6 +712,29 @@ def acceptPreformatted(self, node, memo): acceptUnaryMinus = makeNodePreformattedExpr('-' + FS.l) acceptUnaryPlus = makeNodePreformattedExpr('+' + FS.l) + def acceptCastExpr(self, node, memo): + """ Accept and process a cast expression. """ + # If the type of casting is a primitive type, + # then do the cast, else drop it. + factory = self.factory.expr + typeTok = node.firstChildOfType(tokens.TYPE) + typeIdent = typeTok.firstChild() + typeName = typeIdent.text + if typeIdent.type == tokens.QUALIFIED_TYPE_IDENT: + typeName = typeIdent.firstChild().text + + if typeName in tokens.primitiveTypeNames: + # Cast using the primitive type constructor + self.fs = typeName + '(' + FS.r + ')' + else: + handler = self.configHandler('Cast') + if handler: + handler(self, node) + else: + warn('No handler to perform cast of non-primitive type %s.', typeName) + self.left, self.right = visitors = factory(parent=self), factory(parent=self) + self.zipWalk(node.children, visitors, memo) + def makeAcceptPrePost(suffix, pre): """ Make an accept method for pre- and post- assignment expressions. """ def acceptPrePost(self, node, memo): @@ -802,7 +847,7 @@ def acceptThisConstructorCall(self, node, memo): def acceptStaticArrayCreator(self, node, memo): """ Accept and process a static array expression. """ - self.right = self.factory.expr(fs='[None]*{left}') + self.right = self.factory.expr(fs='[None] * {left}') self.right.left = self.factory.expr() self.right.left.walk(node.firstChildOfType(tokens.EXPR), memo) diff --git a/java2python/config/__init__.py b/java2python/config/__init__.py index 72d6c5e..2aa8387 100644 --- a/java2python/config/__init__.py +++ b/java2python/config/__init__.py @@ -27,7 +27,7 @@ def last(self, key, default=None): @staticmethod def load(name): """ Imports and returns a module from dotted form or filename. """ - if path.exists(name): + if path.exists(name) and path.isfile(name): mod = load_source(str(hash(name)), name) else: mod = reduce(getattr, name.split('.')[1:], __import__(name)) diff --git a/java2python/config/default.py b/java2python/config/default.py index a6a49c1..92c4a27 100644 --- a/java2python/config/default.py +++ b/java2python/config/default.py @@ -24,7 +24,9 @@ modulePrologueHandlers = [ basic.shebangLine, basic.simpleDocString, + 'from __future__ import print_function', basic.maybeBsr, + basic.maybeAbstractHelpers, basic.maybeSyncHelpers, ] @@ -97,9 +99,9 @@ methodPrologueHandlers = [ basic.maybeAbstractMethod, basic.maybeClassMethod, + basic.overloadedClassMethods, # NB: synchronized should come after classmethod basic.maybeSynchronizedMethod, - basic.overloadedClassMethods, ] @@ -131,7 +133,7 @@ # This handler is turns java imports into python imports. No mapping # of packages is performed: -moduleImportDeclarationHandler = basic.simpleImports +# moduleImportDeclarationHandler = basic.simpleImports # This import decl. handler can be used instead to produce comments # instead of import statements: @@ -148,15 +150,26 @@ (Type('TRUE'), transform.true2True), (Type('IDENT'), transform.keywordSafeIdent), + (Type('DECIMAL_LITERAL'), transform.syntaxSafeDecimalLiteral), + (Type('FLOATING_POINT_LITERAL'), transform.syntaxSafeFloatLiteral), - (Type('FLOATING_POINT_LITERAL'), - transform.syntaxSafeFloatLiteral), + (Type('TYPE') > Type('BOOLEAN'), transform.typeSub), + (Type('TYPE') > Type('BYTE'), transform.typeSub), + (Type('TYPE') > Type('CHAR'), transform.typeSub), + (Type('TYPE') > Type('FLOAT'), transform.typeSub), + (Type('TYPE') > Type('INT'), transform.typeSub), + (Type('TYPE') > Type('SHORT'), transform.typeSub), + (Type('TYPE') > Type('LONG'), transform.typeSub), + (Type('TYPE') > Type('DOUBLE'), transform.typeSub), - (Type('TYPE') > Type('BOOLEAN'), - transform.typeSub), + (Type('METHOD_CALL') > Type('DOT') > Type('IDENT', 'length'), + transform.lengthToLen), - (Type('TYPE') > Type('DOUBLE'), - transform.typeSub), + (Type('METHOD_CALL') > Type('DOT') > ( + Type('IDENT', 'String') + + Type('IDENT', 'format') + ), + transform.formatString), (Type('TYPE') > Type('QUALIFIED_TYPE_IDENT') > Type('IDENT'), transform.typeSub), @@ -170,6 +183,12 @@ # in method declarations. set to 0 to disable. #minIndentParams = 5 +# Specifies handler for cast operations of non-primitive types are handled +# (primitive types are automatically handled). Use basic.castDrop to leave +# cast expressions out of generated source. Use basic.castCtor to transform +# casts into constructor calls. Or you can specify a function of your own. +expressionCastHandler = basic.castDrop + # Values below are used by the handlers. They're here for # convenience. @@ -177,8 +196,8 @@ # module output subs. moduleOutputSubs = [ - (r'System\.out\.println\((.*)\)', r'print \1'), - (r'System\.out\.print_\((.*?)\)', r'print \1,'), + (r'System\.out\.println\((.*)\)', r'print(\1)'), + (r'System\.out\.print_\((.*?)\)', r'print(\1, end="")'), (r'(.*?)\.equals\((.*?)\)', r'\1 == \2'), (r'(.*?)\.equalsIgnoreCase\((.*?)\)', r'\1.lower() == \2.lower()'), (r'([\w.]+)\.size\(\)', r'len(\1)'), @@ -186,25 +205,47 @@ (r'(\s)(\S*?)(\.toString\(\))', r'\1\2.__str__()'), (r'(\s)def toString', r'\1def __str__'), (r'(\s)(\S*?)(\.toLowerCase\(\))', r'\1\2.lower()'), - (r'(\s)(\S*?)(\.length\(\))', r'\1len(\2)'), (r'(.*?)IndexOutOfBoundsException\((.*?)\)', r'\1IndexError(\2)'), (r'\.__class__\.getName\(\)', '.__class__.__name__'), (r'\.getClass\(\)', '.__class__'), (r'\.getName\(\)', '.__name__'), (r'\.getInterfaces\(\)', '.__bases__'), - #(r'String\.valueOf\((.*?)\)', r'str(\1)'), + (r'String\.valueOf\((.*?)\)', r'str(\1)'), #(r'(\s)(\S*?)(\.toString\(\))', r'\1str(\2)'), + (r'Math\.', ''), ] typeSubs = { 'Boolean' : 'bool', 'boolean' : 'bool', - 'IndexOutOfBoundsException' : 'IndexError', - 'Integer' : 'int', - 'Object' : 'object', - 'String' : 'str', + + 'Byte' : 'int', + 'byte' : 'int', + + 'Char' : 'str', 'char' : 'str', + + 'Integer' : 'int', + 'int' : 'int', + + 'Short' : 'int', + 'short' : 'int', + + 'Long' : 'long', + 'long' : 'long', + + 'Float' : 'float', + 'float' : 'float', + + 'Double' : 'float', 'double' : 'float', + + 'String' : 'str', 'java.lang.String' : 'str', -} + + 'Object' : 'object', + + 'IndexOutOfBoundsException' : 'IndexError', + 'IOException': 'IOError', + } diff --git a/java2python/lang/base.py b/java2python/lang/base.py index 4c10110..0633b8e 100644 --- a/java2python/lang/base.py +++ b/java2python/lang/base.py @@ -88,6 +88,11 @@ def methodTypes(self): mod = self.module return (mod.VOID_METHOD_DECL, mod.FUNCTION_METHOD_DECL, ) + @property + def primitiveTypeNames(self): + """ Type name of well-known primitive types """ + return ('bool', 'str', 'int', 'long', 'float', ) + @property def module(self): """ Provides lazy import to the parser module. """ diff --git a/java2python/mod/basic.py b/java2python/mod/basic.py index e226545..02e2f57 100644 --- a/java2python/mod/basic.py +++ b/java2python/mod/basic.py @@ -7,6 +7,8 @@ from os import path from re import sub as rxsub +from java2python.lib import FS + def shebangLine(module): """ yields the canonical python shebang line. """ @@ -34,7 +36,7 @@ def simpleDocString(obj): def commentedImports(module, expr): - module.factory.comment(parent=module, left=expr, fs='import: {left}') + module.factory.comment(parent=module, left=expr, fs='#import {left}') def simpleImports(module, expr): @@ -107,11 +109,13 @@ def overloadedClassMethods(method): cls = method.parent methods = [o for o in cls.children if o.isMethod and o.name==method.name] if len(methods) == 1: + if methods[0].overloaded: + yield methods[0].overloaded return for i, m in enumerate(methods[1:]): args = [p['type'] for p in m.parameters] args = ', '.join(args) - m.decorators.append('@{0}.register({1})'.format(method.name, args)) + m.overloaded = '@{0}.register({1})'.format(method.name, args) m.name = '{0}_{1}'.format(method.name, i) # for this one only: yield '@overloaded' @@ -129,8 +133,6 @@ def maybeAbstractMethod(method): def maybeSynchronizedMethod(method): if 'synchronized' in method.modifiers: - module = method.parents(lambda x:x.isModule).next() - module.needsSyncHelpers = True yield '@synchronized' @@ -156,6 +158,11 @@ def maybeBsr(module): yield line +def maybeAbstractHelpers(module): + if getattr(module, 'needsAbstractHelpers', False): + yield 'from abc import ABCMeta, abstractmethod' + + def maybeSyncHelpers(module): if getattr(module, 'needsSyncHelpers', False): for line in getSyncHelpersSrc().split('\n'): @@ -237,3 +244,9 @@ def moveStaticExpressions(cls): module.adopt(newExpr, index=len(module.children)) +def castCtor(expr, node): + expr.fs = FS.l + '(' + FS.r + ')' + + +def castDrop(expr, node): + expr.fs = FS.r diff --git a/java2python/mod/include/classmethod.py b/java2python/mod/include/classmethod.py new file mode 100644 index 0000000..cb97954 --- /dev/null +++ b/java2python/mod/include/classmethod.py @@ -0,0 +1,6 @@ +class classmethod_(classmethod): + """ Classmethod that provides attribute delegation. + + """ + def __getattr__(self, name): + return getattr(self.__func__, name) diff --git a/java2python/mod/include/overloading.py b/java2python/mod/include/overloading.py index d7baafd..2f8257c 100644 --- a/java2python/mod/include/overloading.py +++ b/java2python/mod/include/overloading.py @@ -36,11 +36,13 @@ """ -import new +from types import MethodType as instancemethod -# Make the environment more like Python 3.0 -__metaclass__ = type -from itertools import izip as zip +import sys +if sys.version_info[0] < 3: + # Make the environment more like Python 3.0 + __metaclass__ = type + from itertools import izip as zip class overloaded: @@ -55,7 +57,7 @@ def __init__(self, default_func): def __get__(self, obj, type=None): if obj is None: return self - return new.instancemethod(self, obj) + return instancemethod(self, obj) def register(self, *types): """Decorator to register an implementation for a specific set of types. diff --git a/java2python/mod/include/sync.py b/java2python/mod/include/sync.py index 8201698..ddac055 100644 --- a/java2python/mod/include/sync.py +++ b/java2python/mod/include/sync.py @@ -1,12 +1,13 @@ +from functools import wraps from threading import RLock -_locks = {} -def lock_for_object(obj, locks=_locks): +def lock_for_object(obj, locks={}): return locks.setdefault(id(obj), RLock()) - def synchronized(call): + assert call.__code__.co_varnames[0] in ['self', 'cls'] + @wraps(call) def inner(*args, **kwds): - with lock_for_object(call): + with lock_for_object(args[0]): return call(*args, **kwds) return inner diff --git a/java2python/mod/transform.py b/java2python/mod/transform.py index 48d18fd..9b2e567 100644 --- a/java2python/mod/transform.py +++ b/java2python/mod/transform.py @@ -11,9 +11,14 @@ # See the java2python.config.default and java2python.lang.selector modules to # understand how and when selectors are associated with these callables. +import re +from logging import warn + import keyword import types +from java2python.lang import tokens + def invalidPythonNames(): """ Creates a list of valid Java identifiers that are invalid in Python. """ @@ -43,18 +48,141 @@ def xform(node, config): true2True = makeConst('True') +def syntaxSafeDecimalLiteral(node, config): + """ Ensures a Java decimal literal is a valid Python decimal literal. """ + value = node.token.text + if value.endswith(('l', 'L')): + value = value[:-1] + node.token.text = value + + def syntaxSafeFloatLiteral(node, config): """ Ensures a Java float literal is a valid Python float literal. """ value = node.token.text if value.startswith('.'): value = '0' + value - if value.endswith(('f', 'd')): + if value.lower().endswith(('f', 'd')): value = value[:-1] - elif value.endswith(('l', 'L')): - value = value[:-1] + 'L' node.token.text = value +def lengthToLen(node, config): + """ Transforms expressions like 'value.length()' to 'len(value)'. + + This method changes AST branches like this: + + METHOD_CALL [start=45, stop=49] + DOT . [line=4, start=45, stop=47] + IDENT foo [line=4, start=45] + IDENT length [line=4, start=47] + ARGUMENT_LIST [line=4, start=48, stop=49] + + Into branches like this: + + IDENT len(foo) [line=4, start=45] + + Notice that the resulting IDENT node text is invalid. We can't use a + METHOD_CALL token because those are always bound to a class or instance. + It would be best to add a new token type, and that option will be explored + if we run into this problem again. + + """ + dot = node.parent + method = dot.parent + + ident = dot.firstChildOfType(tokens.IDENT) + ident.token.text = 'len({})'.format(ident.text) + + expr = method.parent + expr.children.remove(method) + expr.addChild(ident) + + +def formatSyntaxTransf(match): + """ Helper function for formatString AST transform. + + Translates the Java Formatter syntax into Python .format syntax. + + This function gets called by re.sub which matches all the %...$... groups + inside a format specifier string. + """ + groups = match.groupdict() + if groups['convers'] == 'n': + # Means platform-specific line separator + return '\\n' # Py converts \n to os.linesep + + result = '{' + thous_sep = '' + + if(groups['idx']): + idx = int(groups['idx'][:-1]) + result += str(idx - 1) # Py starts count from 0 + result += ':' + + if(groups['flags']): + if ',' in groups['flags']: + thous_sep = ',' + if '+' in groups['flags']: + result += '+' + elif ' ' in groups['flags']: + result += ' ' + if '#' in groups['flags']: + result += '#' + if '0' in groups['flags']: + result += '0' + + if(groups['width']): + result += groups['width'] + result += thous_sep + + if(groups['precision']): + result += groups['precision'] + + result += groups['convers'] + '}' + + return result + +def formatString(node, config): + """ Transforms string formatting like 'String.format("%d %2$s", i, s)' + into '"{:d} {2:s}".format(i, s)'. + """ + dot = node.parent + method = dot.parent + arg_list = method.firstChildOfType(tokens.ARGUMENT_LIST) + call_args = [arg for arg in arg_list.childrenOfType(tokens.EXPR)] + args = [arg.firstChildOfType(tokens.IDENT) for arg in call_args[1:]] + + # Translate format syntax (if format == string_literal) + format = call_args[0].firstChildOfType(tokens.STRING_LITERAL) + if format: + format.token.text = \ + re.sub(r'%(?P\d+\$)?(?P[-+# 0,]+)?(?P[0-9]+)?(?P\.[0-9]+)?(?P[scdoxefgn])', + formatSyntaxTransf, + format.token.text, + flags=re.IGNORECASE) + else: + # Translation should happen at runtime + format = call_args[0].firstChild() + if format.type == tokens.IDENT: + # String variable + warn('Formatting string %s is not automatically translated.' + % str(format.token.text)) + else: + # Function that returns String + warn('Formatting string returned by %s() is not automatically translated.' + % str(format.firstChildOfType(tokens.IDENT).token.text)) + + left_ident = dot.children[0] + right_ident = dot.children[1] + + # Change AST + arg_list.children.remove(format.parent) + dot.children.remove(left_ident) + dot.children.remove(right_ident) + dot.addChild(format) + dot.addChild(right_ident) + + def typeSub(node, config): """ Maps specific, well-known Java types to their Python counterparts. @@ -62,6 +190,7 @@ def typeSub(node, config): mapping and further discussion. """ ident = node.token.text - subs = config.last('typeSubs') - if ident in subs: - node.token.text = subs[ident] + for subs in reversed(config.every('typeSubs', {})): + if ident in subs: + node.token.text = subs[ident] + return diff --git a/readme.md b/readme.md index 39af789..f38d91a 100644 --- a/readme.md +++ b/readme.md @@ -21,7 +21,7 @@ Here's a very simple example: ```bash $ cat HelloWorld.java ``` -```java +```java // This is the HelloWorld class with a single method. class HelloWorld { public static void main(String[] args) { @@ -60,4 +60,4 @@ if __name__ == '__main__': [introduction]: https://github.com/natural/java2python/tree/master/doc/intro.md [lots of docs]: https://github.com/natural/java2python/tree/master/doc/ [many options]: https://github.com/natural/java2python/tree/master/doc/customization.md -[plenty of tests]: https://github.com/natural/java2python/tree/master/test/ +[plenty of tests]: https://github.com/natural/java2python/tree/master/doc/tests.md diff --git a/setup.py b/setup.py index 604faa5..210d623 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def doc_files(): setup( name='java2python', - version='0.5.0', + version='0.5.1', description=description, long_description=long_description, @@ -47,7 +47,7 @@ def doc_files(): author_email='troy@troy.io', url='https://github.com/natural/java2python/', - download_url='https://github.com/downloads/natural/java2python/java2python-0.5.0.tar.gz', + download_url='https://github.com/downloads/natural/java2python/java2python-0.5.1.tar.gz', keywords=['java', 'java2python', 'compiler'], classifiers=filter(None, classifiers.split('\n')), @@ -59,6 +59,7 @@ def doc_files(): 'java2python.lang', 'java2python.lib', 'java2python.mod', + 'java2python.mod.include', ], package_data={ diff --git a/test/BasicTypes1.java b/test/BasicTypes1.java new file mode 100644 index 0000000..60c43b8 --- /dev/null +++ b/test/BasicTypes1.java @@ -0,0 +1,17 @@ +class BasicTypes1 { + char chr; + byte byt; + short shr; + int in_t; + long lng; + float flt; + double dbl; + + public static void main(String[] args) { + BasicTypes1 bt1 = new BasicTypes1(); + System.out.println(bt1.byt); + System.out.println(bt1.shr); + System.out.println(bt1.in_t); + } + +} diff --git a/test/BasicTypes2.java b/test/BasicTypes2.java new file mode 100644 index 0000000..5737eef --- /dev/null +++ b/test/BasicTypes2.java @@ -0,0 +1,33 @@ +class BasicTypes2 { + public static void main(String[] args) { + + Integer I = 32; + System.out.println(I); + + Boolean B = true; + System.out.println( B ? "ok" : "nope" ); + + Byte Y = 0; + System.out.println(Y); + + Character C = 'c'; + System.out.println(C); + + Short S = 128; + System.out.println(S); + + Long L = 128L; + System.out.println(L); + + Float F = 1.5F; + System.out.println(F); + + Double D = 1.5; + System.out.println(D); + + String T = "done"; + System.out.println(T); + + + } +} diff --git a/test/BasicTypes3.java b/test/BasicTypes3.java new file mode 100644 index 0000000..76821a2 --- /dev/null +++ b/test/BasicTypes3.java @@ -0,0 +1,20 @@ +class Vector {} + +class BasicTypes3 { + Boolean B; + Integer I; + Double D; + + String S; + Vector V; + + public static void main(String[] args) { + BasicTypes3 bt3 = new BasicTypes3(); + System.out.println(bt3.B == null ? 1 : 0); + System.out.println(bt3.I == null ? 1 : 0); + System.out.println(bt3.D == null ? 1 : 0); + System.out.println(bt3.S == null ? 1 : 0); + System.out.println(bt3.V == null ? 1 : 0); + } + +} diff --git a/test/Cast0.java b/test/Cast0.java new file mode 100644 index 0000000..c680377 --- /dev/null +++ b/test/Cast0.java @@ -0,0 +1,40 @@ +interface Something { + public int foo(); +} + + +interface SomethingElse { + public int foo(); +} + + +class Both implements Something { + public int foo() { + return 100; + } +} + +class What extends Both { + public int foo() { + return 200; + } +} + + +class Cast0 { + public static void main(String[] args) { + int x = 33; + Integer ix = (Integer) x; + System.out.println(x); + System.out.println(ix); + + What w = new What(); + + System.out.println( w.foo() ); + + Both b = (Both) w; + + System.out.println( b.foo() ); + + } +} \ No newline at end of file diff --git a/test/Comments4.java b/test/Comments4.java new file mode 100644 index 0000000..04a19ec --- /dev/null +++ b/test/Comments4.java @@ -0,0 +1,12 @@ +// Header Comment + +class Comments4 { + void method() { + for (;;) { } + } + + public static void main(String[] args) { + System.out.println("Comments4."); + } + +} diff --git a/test/Continue0.java b/test/Continue0.java index 727eaef..d7bc322 100644 --- a/test/Continue0.java +++ b/test/Continue0.java @@ -3,12 +3,12 @@ public static void main(String[] args) { int x = 0; while (x < 10) { System.out.println(x); - if (x==6) { - break ; + if (x == 6) { + break; } else { - x+=2; - continue ; + x += 2; + continue; } - } + } } } diff --git a/test/Continue1.java b/test/Continue1.java new file mode 100644 index 0000000..a3e1443 --- /dev/null +++ b/test/Continue1.java @@ -0,0 +1,13 @@ +class Continue1 { + public static void main(String[] args) { + int[] ints = {1, 2, 3, 4, 5, 6, 7}; + for (int x : ints) { + if (x == 6) { + break; + } else if (x == 3) { + continue; + } + System.out.println(x); + } + } +} diff --git a/test/Continue2.java b/test/Continue2.java new file mode 100644 index 0000000..506e10f --- /dev/null +++ b/test/Continue2.java @@ -0,0 +1,14 @@ +class Continue2 { + public static void main(String[] args) { + int x = 0; + do { + System.out.println(x); + if (x == 6) { + break; + } else { + x += 2; + continue; + } + } while (x < 10); + } +} diff --git a/test/Exception0.java b/test/Exception0.java new file mode 100644 index 0000000..f784749 --- /dev/null +++ b/test/Exception0.java @@ -0,0 +1,17 @@ +import java.io.IOException; + +class Exception0 { + static void test() throws IOException { + throw new IOException("test"); + } + + public static void main(String[] args) { + try { + test(); + } catch (IOException e) { + System.out.println("catch"); + } finally { + System.out.println("done"); + } + } +} diff --git a/test/ForLoop2.java b/test/ForLoop2.java new file mode 100644 index 0000000..6c72ee2 --- /dev/null +++ b/test/ForLoop2.java @@ -0,0 +1,9 @@ +class ForLoop2 { + public static void main(String[] args) { + for (int i = 0; i < 3; i++) { + if (i == 1) + continue; + System.out.println(i); + } + } +} diff --git a/test/Format0.java b/test/Format0.java new file mode 100644 index 0000000..68a22a4 --- /dev/null +++ b/test/Format0.java @@ -0,0 +1,9 @@ +public class Format0 { + public static void main(String[] args) { + int i = 22; + String s = "text"; + String r = String.format("> (%1$d) %n %2$s", i, s); + + System.out.println(r); + } +} diff --git a/test/Format1.java b/test/Format1.java new file mode 100644 index 0000000..29e94ee --- /dev/null +++ b/test/Format1.java @@ -0,0 +1,19 @@ +public class Format1 { + public static void main(String[] args) { + long n = 461012; + String f1 = String.format("%d%n", n); // --> "461012" + String f2 = String.format("%08d%n", n); // --> "00461012" + String f3 = String.format("%+8d%n", n); // --> " +461012" + String f4 = String.format("%,8d%n", n); // --> " 461,012" + String f5 = String.format("%+,8d%n", n); // --> "+461,012" + + System.out.println(f1 + f2 + f3 + f4 + f5); + + double pi = 3.14159265; + String pf1 = String.format("%f%n", pi); // --> "3.141593" + String pf2 = String.format("%.3f%n", pi); // --> "3.142" + String pf3 = String.format("%10.3f%n", pi); // --> " 3.142" + + System.out.println(pf1 + pf2 + pf3); + } +} diff --git a/test/If6.java b/test/If6.java new file mode 100644 index 0000000..ff16faf --- /dev/null +++ b/test/If6.java @@ -0,0 +1,10 @@ +class If6 { + public static void main(String[] args) { + if (false) { + System.out.println("fail"); + } else if (true) { + System.out.println("okay"); + } + } + +} diff --git a/test/If7.java b/test/If7.java new file mode 100644 index 0000000..4cf1c56 --- /dev/null +++ b/test/If7.java @@ -0,0 +1,10 @@ +class If7 { + public static void main(String[] args) { + if (false) { + System.out.println("fail 1"); + } else if (false) { + System.out.println("fail 2"); + } else { + } + } +} \ No newline at end of file diff --git a/test/If8.java b/test/If8.java new file mode 100644 index 0000000..37e1c46 --- /dev/null +++ b/test/If8.java @@ -0,0 +1,8 @@ +class If8 { + public static void main(String[] args) { + if (true) { + int y = 1; + System.out.println(0 + y); + } + } +} diff --git a/test/Length0.java b/test/Length0.java new file mode 100644 index 0000000..e13f429 --- /dev/null +++ b/test/Length0.java @@ -0,0 +1,11 @@ +class Length0 { + public static int dummy(int v) { + return v + 1; + } + + public static void main(String[] args) { + String foo = "asdf"; + System.out.println( dummy(foo.length() ) ); + } + +} \ No newline at end of file diff --git a/test/Makefile b/test/Makefile index f99772e..e432679 100644 --- a/test/Makefile +++ b/test/Makefile @@ -7,7 +7,7 @@ j2py = ../bin/j2py python_files := $(addsuffix .py, $(notdir $(basename $(wildcard *.java)))) test_targets := $(sort $(notdir $(basename $(wildcard *.java)))) -export PYTHONPATH := $(PYTHONPATH):. +export PYTHONPATH := $(PYTHONPATH):.:.. .PHONY: all clean @@ -43,4 +43,4 @@ parsers: @$(j2py) $(addsuffix .java, $(basename $@)) $@ -c configs.defaults -d configs %: %.py - @bash -c "diff -q <($(python) $(addsuffix .py, $@)) <(java -ea $@)" && echo "[PASS] $@" + @bash -c "diff -u <($(python) $(addsuffix .py, $@)) <(java -ea $@)" && echo "[PASS] $@" diff --git a/test/Math0.java b/test/Math0.java new file mode 100644 index 0000000..67ae731 --- /dev/null +++ b/test/Math0.java @@ -0,0 +1,6 @@ +class Math0 { + public static void main(String[] args) { + System.out.println(Math.abs(-42)); + System.out.println(Math.abs(-0.5)); + } +} diff --git a/test/Self0.java b/test/Self0.java new file mode 100644 index 0000000..c7556ba --- /dev/null +++ b/test/Self0.java @@ -0,0 +1,25 @@ +class Self0 { + private int v1 = 100; + public int v2 = 2; + + public int test0(){ + return v2 + v1; + } + + public boolean test1(){ + return (v1 == v2 || v2 < v1 ); + } + + public static void main(String[] args) { + Self0 s = new Self0(); + System.out.println(s.test0()); + if(s.test1()) + System.out.println("True"); + else + System.out.println("False"); + System.out.print("test"); + System.out.print("ing"); + System.out.println(); + } + +} diff --git a/test/String0.java b/test/String0.java new file mode 100644 index 0000000..e4ed565 --- /dev/null +++ b/test/String0.java @@ -0,0 +1,9 @@ +class String0 { + static void test(String s) { + System.out.println(s); + } + + public static void main(String[] args) { + test(String.valueOf(42)); + } +} diff --git a/test/Synchronized2.java b/test/Synchronized2.java new file mode 100644 index 0000000..3ca9fa5 --- /dev/null +++ b/test/Synchronized2.java @@ -0,0 +1,36 @@ +class Synchronized2 { + + public synchronized void test1() { + System.out.println(1); + } + + public synchronized void test1(String s) { + System.out.println(s); + } + + public static synchronized void test1(int i) { + System.out.println(i); + } + + public static synchronized void test2() { + System.out.println(2); + } + + public static synchronized void test2(String s) { + System.out.println(s); + } + + public synchronized void test2(int i) { + System.out.println(i); + } + + public static void main(String[] args) { + Synchronized2 obj = new Synchronized2(); + obj.test1(); + obj.test1("test1"); + obj.test1(1); + obj.test2(); + obj.test2("test2"); + obj.test2(2); + } +} diff --git a/test/configs/Cast0.py b/test/configs/Cast0.py new file mode 100644 index 0000000..5580ba0 --- /dev/null +++ b/test/configs/Cast0.py @@ -0,0 +1,3 @@ +from java2python.mod import basic + +expressionCastHandler = basic.castDrop diff --git a/test/configs/Class10.py b/test/configs/Class10.py index 71de720..0b340ee 100644 --- a/test/configs/Class10.py +++ b/test/configs/Class10.py @@ -1,6 +1,3 @@ -from java2python.config.default import modulePrologueHandlers - -modulePrologueHandlers += [ +modulePrologueHandlers = [ 'from java2python.mod.include.overloading import overloaded', - 'from abc import ABCMeta, abstractmethod', ] diff --git a/test/configs/Interface1.py b/test/configs/Interface1.py deleted file mode 100644 index 79bd196..0000000 --- a/test/configs/Interface1.py +++ /dev/null @@ -1,5 +0,0 @@ -from java2python.config.default import modulePrologueHandlers - -modulePrologueHandlers += [ - "from abc import ABCMeta, abstractmethod", - ] diff --git a/test/configs/UsePackage0.py b/test/configs/UsePackage0.py new file mode 100644 index 0000000..243c245 --- /dev/null +++ b/test/configs/UsePackage0.py @@ -0,0 +1,3 @@ +from java2python.mod import basic + +moduleImportDeclarationHandler = basic.simpleImports diff --git a/test/configs/defaults.py b/test/configs/defaults.py index c822d48..7a5d639 100644 --- a/test/configs/defaults.py +++ b/test/configs/defaults.py @@ -1,11 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from java2python.config import default - - -modulePrologueHandlers = default.modulePrologueHandlers + [ +modulePrologueHandlers = [ + 'from java2python.mod.include.classmethod import classmethod_ as classmethod', 'from java2python.mod.include.overloading import overloaded', - 'from abc import ABCMeta, abstractmethod', 'import zope.interface', ] diff --git a/test/runj2py b/test/runj2py index d35a109..b338a39 100755 --- a/test/runj2py +++ b/test/runj2py @@ -1,2 +1,2 @@ #!/bin/bash -../bin/j2py -d configs "$@" +PYTHONPATH=$PYTHONPATH:.. ../bin/j2py -d configs "$@"