Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 173 additions & 1 deletion clamp/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from clamp.proxymaker import ClampProxyMaker
from clamp.signature import Constant


def clamp_base(package, proxy_maker=ClampProxyMaker):
""" A helper method that allows you to create clamped classes

Expand Down Expand Up @@ -41,3 +40,176 @@ class ClampBase(object):
__metaclass__ = _clamp_closure(package=package, proxy_maker=proxy_maker)

return ClampBase



import inspect
import logging

from clamp.signature import ClassInfo, MethodInfo

log = logging.getLogger(__name__)

def clamp_class(package, proxy_maker=ClampProxyMaker):
"""
Clamp a class and rewrite as a Java class.

Example::

@clamp_class('org.example.package')
class PythonClass(object):
...
"""
assert isinstance(package, basestring), "package:{!r} is not a string.".format(package)

def class_decorator(target):
log.debug("clamp_class({!r}, {!r})({!r})".format(package, proxy_maker, target))
assert inspect.isclass(target), "target:{!r} is not a class.".format(target)

# Check to see if the class was compiled by the default proxy maker.
# Format:
#
# org.python.proxies.<module>$<Class>$<x>
# org.python.proxies.<package>.<module>$<Class>$<x>
#

try:
target_package, target_module = target.__module__.rsplit('.', 1)
except IndexError:
proxy_package = 'org.python.proxies'
proxy_module = target.__module__
proxy_class = target.__name__
else:
proxy_package = 'org.python.proxies.' + target_package
proxy_module = target_module
proxy_class = target.__name__

log.debug("Find proxy base: {}:{}:{}".format(proxy_package, proxy_module, proxy_class))

log.debug("{}".format(target))
bases = []
for base in target.__bases__:
log.debug("- {}".format(base))
if base.__module__ == proxy_package and tuple(base.__name__.split('$')[:-1]) == (proxy_module, proxy_class):
# Found proxy base, extract its bases.
for sub_base in base.__bases__:
log.debug(" - {}".format(sub_base))
bases.append(sub_base)
else:
# Normal base.
bases.append(base)

log.debug(bases)

class ClampProxyMakerMeta(type):

def __new__(mcs, name, bases, dct):
log.debug("ClampProxyMakerMeta({!r}, {!r}, {!r})".format(name, bases, dct))
newdct = dict(dct)
newdct['__metaclass__'] = mcs
newdct['__proxymaker__'] = proxy_maker(package=package)
return type.__new__(mcs, name, bases, newdct)

# Clamp class.
return ClampProxyMakerMeta(target.__name__, tuple(bases), dict(vars(target)))

return class_decorator

def annotate(*args, **fields):
"""
Apply a Java annotation to a Python class or method.

Example::

@annotate(JavaIterface, field1="Value 1", ...)
class PythonClass(object):

@annotate(JavaInterface, field1="Value 1", ...)
def pythonMethod(self):
...

@annotate('arg', JavaInterface, field1="Value 1", ...)
def doSomething(self, arg):
...
"""
if len(args) == 1:
interface = args[0]
return _annotate(interface, fields)
elif len(args) == 2:
arg, interface = args
return _annotate_arg(arg, interface, fields)

raise TypeError("annotate() takes either 1 or 2 positional arguments ({} given).".format(len(args)))

def _annotate(interface, fields):

def annotate_decorator(target):
log.debug("annotate({!r}, {!r})({!r})".format(interface, fields, target))

assert inspect.isclass(target) or inspect.isfunction(target), "target:{!r} is not a class or method.".format(target)

if inspect.isclass(target):
if not hasattr(target, '_clamp'):
target._clamp = ClassInfo(target)
info = target._clamp
elif inspect.isfunction(target):
# Require methods to be decorated with @method in order to detect
# missing type information early.
assert hasattr(target, '_clamp'), "method:{!r} has not been decorated with @method.".format(target)
info = target._clamp

# Add annotation.
info.annotate(interface, **fields)

return target

return annotate_decorator

def _annotate_arg(arg, interface, fields):

def annotate_decorator(method):
log.debug("annotate({!r}, {!r}, {!r})({!r})".format(arg, interface, fields, method))
method._clamp.annotate_arg(arg, interface, **fields)
return method

return annotate_decorator

def method(return_type, arg_types=None, name=None, access=None):
"""
Apply Java type information the method. This is required in order to
expose any Python methods in a clamped class.

Example::

@method(java.lang.Void)
def doNothing(self):
pass

@method(java.lang.String, (java.lang.Long, java.lang.Long))
def someMethod(self, value1, value2):
return str(value1 + value2)
"""
def method_decorator(method):
log.debug("method({!r}, {!r}, {!r})({!r})".format(return_type, arg_types, name, method))
method._clamp = MethodInfo(method, return_type, arg_types=arg_types, name=name, access=access)
return method

return method_decorator

def throws(*exception_types):
"""
Declare what Java exceptions the method throws.

Example::

@throws(java.io.FileNotFoundException)
@method(java.lang.Void.TYPE)
def doNothing(self):
pass
"""
def throws_decorator(method):
log.debug("throws(*{!r})({!r})".format(exception_types, method))
method._clamp.throws(*exception_types)
return method

return throws_decorator
140 changes: 138 additions & 2 deletions clamp/proxymaker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import inspect
import logging
import re

import java
from java.io import Serializable
Expand All @@ -10,6 +12,7 @@
from clamp.build import get_builder
from clamp.signature import Constant

_MATCH_PRIVATE_NAME = re.compile('^_(?P<class>\w+)__(?P<attribute>\w+)$')

log = logging.getLogger(__name__)

Expand All @@ -33,9 +36,10 @@ class SerializableProxyMaker(CustomMaker):
# {'__init__': <function __init__ at 0x2>, '__module__': '__main__', 'call': <function call at 0x3>, '__proxymaker__': <clamp.ClampProxyMaker object at 0x4>}, 'clamped', {})

def __init__(self, superclass, interfaces, className, pythonModuleName, fullProxyName, mapping, package, kwargs):
self.mapping = mapping
self.package = package
self.kwargs = kwargs

log.debug("superclass=%s, interfaces=%s, className=%s, pythonModuleName=%s, fullProxyName=%s, mapping=%s, "
"package=%s, kwargs=%s", superclass, interfaces, className, pythonModuleName, fullProxyName, mapping,
package, kwargs)
Expand Down Expand Up @@ -104,13 +108,95 @@ def makeClass(self):
raise TypeError("Cannot clamp proxy class {} without a defined builder".format(self.myClass))
return cls

def visitClassAnnotations(self):
# Find class annotations.
class_info = self.mapping.get('_clamp')
if class_info is not None:
for annotation in class_info.annotations:
self.addClassAnnotation(annotation)

def visitMethods(self, *args):
# Only override the signature:
#
# void visitMethods()
#
if args:
return self.super__visitMethods(*args)

# Add default methods.
self.super__visitMethods()

# Add methods with type information.
for name, method in self.mapping.iteritems():
if isinstance(method, (classmethod, staticmethod)):
log.warning("method:{!r} is not yet supported.".format(method))
continue

if not inspect.isfunction(method):
continue

method_info = getattr(method, '_clamp', None)
if method_info is None:
continue

if method_info.return_type is None or method_info.arg_types is None:
log.warning("method:{!r} info:{!r} is missing type information.".format(method, method_info))
continue

if method_info.access is not None:
access = method_info.access
else:
# Deduce method modifer from name and decorators.
# - TODO: How should @classmethod be handled? Should it be STATIC?
access = access_from_name(name)

if isinstance(method, staticmethod):
# Interpret:
#
# @staticmethod
# def func(...)
#
# As:
#
# static func(...)
#
access |= Modifier.STATIC

if getattr(method, '__isabstractmethod__', False):
# Interpret:
#
# @abc.abstractmethod
# def func(self, ...)
#
# As:
#
# abstract func(...)
#
access |= Modifier.ABSTRACT

# Slice off the beginning *self* argument.
# - TODO: This will need to be modified once @staticmethod and
# @classmethod are supported.
argspec = inspect.getargspec(method)
args = argspec.args[1:]

arg_annotations = [method_info.arg_annotations[arg] for arg in args]

# HACK: This causes the methods to be generated without a call to
# the super-class. This prevents a `NullPointerException` from
# being raised because of a lack of passing the non-existant
# class.
access |= Modifier.ABSTRACT

self.addMethod(method_info.name, name, method_info.return_type, method_info.arg_types, method_info.exception_types, access, None, method_info.annotations, arg_annotations)


class ClampProxyMaker(object):

def __init__(self, package, **kwargs):
self.package = package
self.kwargs = kwargs

def __call__(self, superclass, interfaces, className, pythonModuleName, fullProxyName, mapping):
"""Constructs a usable proxy name that does not depend on ordering"""
log.debug("Called ClampProxyMaker: %s, %r, %r, %s, %s, %s, %r", self.package, superclass, interfaces,
Expand All @@ -119,3 +205,53 @@ def __call__(self, superclass, interfaces, className, pythonModuleName, fullProx
superclass, interfaces, className, pythonModuleName,
self.package + "." + pythonModuleName + "." + className, mapping,
self.package, self.kwargs)


def access_from_name(name):
"""
Derive the method or field access from its name.
"""
result = _MATCH_PRIVATE_NAME.match(name)
if result is not None:
# Interpret:
#
# def __func(self, ...)
#
# As:
#
# private __func(...)
#
return Modifier.PRIVATE

elif name.startswith('__') and name.endswith('__'):
# Interpret:
#
# def __func__(self, ...)
#
# As:
#
# public __func__(...)
#
return Modifier.PUBLIC

elif name.startswith('_'):
# Interpret:
#
# def _func(self, ...)
#
# As:
#
# protected _func(...)
#
return Modifier.PROTECTED

else:
# Interpret:
#
# def func(self, ...)
#
# As:
#
# public func(...)
#
return Modifier.PUBLIC
Loading