Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension #890

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
34 changes: 34 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,37 @@ liability incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

**END OF TERMS AND CONDITIONS**


==============
ASM License
==============

ASM: a very small and fast Java bytecode manipulation framework
Copyright (c) 2000-2011 INRIA, France Telecom
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holders nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.

95 changes: 95 additions & 0 deletions doc/extension.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
This information is a draft for the user guide.

Java Extensions
===============

JPype can extend Java classes from within Python. Java Extensions are defining
using Python class definitions but they have a number of restrictions. Here is
a summary of the key differences.

- The JVM must be running to define a Java extension type.

- Java extensions use decorators to mark up Python elements for use by Java.
Annotations include ``@JPublic``, ``@JProtected``, ``@JPrivate``,
``@JOverride`` and ``@JThrows``. The concepts of ``final`` and ``static``
are not currently supported.

- ``@JPublic``, ``@JProtected``, or ``@JPrivate`` are considered accessors
decorators. They are used without the ``@`` when declaring fields.

- Java extensions must have all Java base types.

- Java base types must not be final.

- Java bases can have at most one concrete or abstract base type, but they can
have multiple interfaces.

- If no concrete type is included then JObject will be added as the concrete
base.

- All Java extensions are private classes without a package. Thus, they cannot
be annotated with ``@JPublic``, ``@JProtected``, or ``@JPrivate``.

- Java extensions will be Java classes and therefore must have their strongly
types methods and fields.

- All methods must be annotated with an accessor decorator.

- Methods can be annotated with ``@JOverride``. Methods with ``@JOverride``
must have a signature that matches a method in the parent.

- Java methods must take ``this`` as the first argument as opposed to the usual
Python convention ``self``. ``this`` is a Java handle with privilaged access
to private methods and fields.

- Java methods must have a return annotation. The return type can be None if
the method returns ``void``. The return type must be a Java class with the
exception of None.

- Java methods must have all parameters specified with the exception of the
``this``. ``this`` should not have a type specification as the type
of this will be changed to the resulting extension class.

- Java methods may not accept keyword arguments.

- Java methods may not have default values.

- Python decorators ``@staticmethod`` and ``@classmethod`` are not currently supported.

- Java methods can be overloaded so long as the signatures are not conflicting.

- Variadic arguements are not currently supported.

- Java classes are closed so all fields must be defined in advance before being
used. Use ``JPublic``, ``JProtected``, ``JPrivate`` to declare those slots
in advance.

- Java fields are specified using an accessor with arguments of the type
followed by a list of arguments with variable names with the default value.
The default value must be ``None`` with the exception of Java primitives.

- Field names must not be Java keywords.

- The constructor is specified with ``__init__``. Overloading of constructors
is allowed.

- The first call to the Java class must be to the base class initializer.
Accessing any other field or method will produce a ``ValueError``.


Mechanism
---------

WHen a Java class is used as a base type, control is transfered a class builder
by the meta class JClassMeta. The meta class probes all elements in the Python
prototype for decorated elements. Those decoarated elements are turned into a
class description which is passed to Java. Java ASM is then used to construct
the requested classes which is loaded dynamically. The resulting class is then
passed back to Python which calles JClass to produce a Python class wrapper.

The majority of the errors are should be caught when building the class
description. Errors detected at this stage will produce the using
``TypeError`` exceptions. However, in some cases errors in the class
description may result in errors in the class generation or loading phase.
The exceptions from these staget currently produce Java exceptions.

154 changes: 142 additions & 12 deletions jpype/_jclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,108 @@
import _jpype
from ._pykeywords import pysafe
from . import _jcustomizer
import inspect

__all__ = ['JClass', 'JInterface', 'JOverride']
__all__ = ['JClass', 'JInterface', 'JOverride', 'JPublic', 'JProtected',
'JPrivate', 'JThrows']


def JOverride(*args, **kwargs):
class _JFieldDecl(object):
def __init__(self, cls, name, value, modifiers):
self.cls = cls
self.name = name
self.value = value
self.modifiers = modifiers

def __repr__(self):
return "Field(%s,%s)" % (self.cls.__name__, self.name)


def _JMemberDecl(nonlocals, target, strict, modifiers, **kwargs):
"""Generic annotation to pass to the code generator.
"""
if not "__jspec__" in nonlocals:
nonlocals["__jspec__"] = []
jspec = nonlocals["__jspec__"]

if isinstance(target, type):
if not isinstance(target, _jpype.JClass):
raise TypeError("Fields must be Java classes")
prim = issubclass(target, (_jpype._JBoolean, _jpype._JNumberLong, _jpype._JChar, _jpype._JNumberFloat))
out = []
for p, v in kwargs.items():
if not prim and v is not None:
raise ValueError("Initial value must be None")
if prim:
v = _jpype.JObject(v, target) # box it
var = _JFieldDecl(target, p, v, modifiers)
jspec.append(var)
out.append(var)
return out

if isinstance(target, type(_JMemberDecl)):
spec = inspect.getfullargspec(target)
args = spec.args

# Verify the requirements for arguments are met
# Must have a this argument first
if strict:
if len(args) < 1:
raise TypeError("Methods require this argument")
if args[0] != "this":
raise TypeError("Methods first argument must be this")

# All other arguments must be annotated as JClass types
for i in range(1, len(args)):
if not args[i] in spec.annotations:
raise TypeError("Methods types must have specifications")
if not isinstance(spec.annotations[args[i]], _jpype.JClass):
raise TypeError("Method arguments must be Java classes")

if target.__name__ != "__init__":
if "return" not in spec.annotations:
raise TypeError("Return specification required")
if not isinstance(spec.annotations["return"], (_jpype.JClass, type(None))):
raise TypeError("Return type must be Java type")

# Place in the Java spec list
for p, v in kwargs.items():
object.__setattr__(target, p, v)
if modifiers is not None:
jspec.append(target)
object.__setattr__(target, '__jmodifiers__', modifiers)
return target

raise TypeError("Unknown Java specification '%s'" % type(target))


def JPublic(target, **kwargs):
nonlocals = inspect.stack()[1][0].f_locals
return _JMemberDecl(nonlocals, target, True, 1, **kwargs)


def JProtected(target, **kwargs):
nonlocals = inspect.stack()[1][0].f_locals
return _JMemberDecl(nonlocals, target, True, 4, **kwargs)


def JPrivate(target, **kwargs):
nonlocals = inspect.stack()[1][0].f_locals
return _JMemberDecl(nonlocals, target, True, 2, **kwargs)


def JThrows(*args):
for arg in args:
if not isinstance(arg, _jpype.JException):
raise TypeError("JThrows requires Java exception arguments")

def deferred(target):
object.__setattr__(target, '__jthrows__', args)
return target
return deferred


def JOverride(*target, sticky=False, rename=None, **kwargs):
"""Annotation to denote a method as overriding a Java method.

This annotation applies to customizers, proxies, and extensions
Expand All @@ -34,16 +131,22 @@ def JOverride(*args, **kwargs):
sticky=bool: Applies a customizer method to all derived classes.

"""
# Check if called bare
if len(args) == 1 and callable(args[0]):
object.__setattr__(args[0], "__joverride__", {})
return args[0]
# Otherwise apply arguments as needed

def modifier(method):
object.__setattr__(method, "__joverride__", kwargs)
return method
return modifier
nonlocals = inspect.stack()[1][0].f_locals
if len(target) == 0:
overrides = {}
if kwargs:
overrides.update(kwargs)
if sticky:
overrides["sticky"] = True
if rename is not None:
overrides["rename"] = rename

def deferred(method):
return _JMemberDecl(nonlocals, method, False, None, __joverride__=overrides)
return deferred
if len(target) == 1:
return _JMemberDecl(nonlocals, *target, False, None, __joverride__={})
raise TypeError("JOverride can only have one argument")


class JClassMeta(type):
Expand Down Expand Up @@ -240,9 +343,36 @@ def _jclassDoc(cls):
return "\n".join(out)


def _JExtension(name, bases, members):
if "__jspec__" not in members:
raise TypeError("Java classes cannot be extended in Python")
jspec = members['__jspec__']
Factory = _jpype.JClass('org.jpype.extension.Factory')
cls = Factory.newClass(name, bases)
for i in jspec:
if isinstance(i, _JFieldDecl):
cls.addField(i.cls, i.name, i.value, i.modifiers)
elif isinstance(i, type(_JExtension)):
exceptions = getattr(i, '__jthrows__', None)
mspec = inspect.getfullargspec(i)
if i.__name__ == '__init__':
args = [mspec.annotations[j] for j in mspec.args[1:]]
cls.addCtor(args, exceptions, i.__jmodifiers__)
else:
args = [mspec.annotations[j] for j in mspec.args[1:]]
ret = mspec.annotations["return"]
cls.addMethod(i.__name__, ret, args, exceptions, i.__jmodifiers__)
else:
raise TypeError("Unknown member %s" % type(i))
Factory.loadClass(cls)

raise TypeError("Not implemented")


# Install module hooks
_jpype.JClass = JClass
_jpype.JInterface = JInterface
_jpype._jclassDoc = _jclassDoc
_jpype._jclassPre = _jclassPre
_jpype._jclassPost = _jclassPost
_jpype._JExtension = _JExtension
Loading