Skip to content

Commit

Permalink
Adapted to Asphalt 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed May 9, 2016
1 parent 2fe0d76 commit 827ee5f
Show file tree
Hide file tree
Showing 15 changed files with 170 additions and 182 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ sudo: false
language: python

python:
- "3.4"
- "3.5"

install: pip install tox-travis coveralls
Expand All @@ -16,5 +15,7 @@ notifications:
irc:
channels:
- "chat.freenode.net#asphalt"
on_success: change
on_failure: change
use_notice: true
skip_join: true
10 changes: 2 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@
.. image:: https://coveralls.io/repos/asphalt-framework/asphalt-py4j/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/asphalt-framework/asphalt-py4j?branch=master
:alt: Code Coverage
.. image:: https://codeclimate.com/github/asphalt-framework/asphalt-py4j/badges/gpa.svg
:target: https://codeclimate.com/github/asphalt-framework/asphalt-py4j
:alt: Code Climate

This is a component for the Asphalt framework that provides Py4J_ integration.

Py4J allows you to call Java code from Python by either spawning a slave JVM
(`Java Virtual Machine`_) in a subprocess or by connecting to an existing JVM.
This Asphalt framework component provides the ability to run Java code directly from Python.

It is a wrapper for the Py4J_ library.

Project links
-------------
Expand All @@ -22,7 +17,6 @@ Project links
* `Source code`_
* `Issue tracker`_


.. _Py4J: https://www.py4j.org/
.. _Java Virtual Machine: https://en.wikipedia.org/wiki/Java_virtual_machine
.. _Documentation: http://asphalt-py4j.readthedocs.org/en/latest/
Expand Down
109 changes: 51 additions & 58 deletions asphalt/py4j/component.py
Original file line number Diff line number Diff line change
@@ -1,79 +1,74 @@
from asyncio import coroutine
from importlib import import_module
from typing import Dict, Any, Union, Iterable
import logging
import os
import re
from functools import partial
from importlib import import_module
from typing import Dict, Any, Union, Iterable

from py4j.java_gateway import (JavaGateway, launch_gateway, GatewayParameters,
CallbackServerParameters)
from typeguard import check_argument_types
from asphalt.core.component import Component
from asphalt.core.context import Context

from asphalt.core import Component, Context, merge_config

logger = logging.getLogger(__name__)
package_re = re.compile(r'\{(.+?)\}')


class Py4JComponent(Component):
"""
Provides Py4J integration.
Publishes one or more :class:`~py4j.java_gateway.JavaGateway` resources.
Publishes one or more :class:`~py4j.java_gateway.JavaGateway`
objects as resources and context variables.
If ``gateways`` is given, a Java gateway resource will be published for each key in the
dictionary, using the key as the resource name. Any extra keyword arguments to the component
constructor will be used as defaults for omitted configuration values.
:param gateways: a dictionary of JavaGateway resource name ->
:meth:`create_gateway` keyword arguments
:param default_gateway_args: :meth:`create_gateway` arguments for
the default gateway
"""
If ``gateways`` is omitted, a single gateway resource (``default`` / ``ctx.java``)
is published using any extra keyword arguments passed to the component.
package_re = re.compile(r'\{(.+?)\}')
:param gateways: a dictionary of resource name ⭢ :meth:`configure_gateway` arguments
:param default_gateway_args: default values for omitted :meth:`configure_gateway` arguments
"""

def __init__(self, gateways: Dict[str, Dict[str, Any]]=None, **default_gateway_args):
def __init__(self, gateways: Dict[str, Dict[str, Any]] = None, **default_gateway_args):
assert check_argument_types()
if gateways and default_gateway_args:
raise ValueError('specify either a "gateways" dictionary or the default gateway\'s '
'options directly, but not both')

if not gateways:
default_gateway_args.setdefault('context_attr', 'java')
gateways = {'default': default_gateway_args}

self.gateways = [self.create_gateway(alias, **kwargs)
for alias, kwargs in gateways.items()]
self.gateways = []
for resource_name, config in gateways.items():
config = merge_config(default_gateway_args, config)
config.setdefault('context_attr', resource_name)
context_attr, *gateway_settings = self.configure_gateway(**config)
self.gateways.append((resource_name, context_attr) + tuple(gateway_settings))

@classmethod
def create_gateway(cls, resource_name: str, context_attr: str=None, launch_jvm: bool=True,
gateway: Union[GatewayParameters, Dict[str, Any]]=None,
callback_server: Union[CallbackServerParameters, Dict[str, Any]]=False,
javaopts: Iterable[str]=(), classpath: Iterable[str]=''):
def configure_gateway(cls, context_attr: str, launch_jvm: bool = True,
gateway: Union[GatewayParameters, Dict[str, Any]] = None,
callback_server: Union[CallbackServerParameters, Dict[str, Any]] = False,
javaopts: Iterable[str] = (), classpath: Iterable[str] = ''):
"""
Configures a Py4J gateway with the given parameters.
:param resource_name: resource name the mailer will be
published as
:param context_attr: the mailer's attribute name on the context
(defaults to the value of ``resource_name``)
:param launch_jvm: ``True`` to spawn a Java Virtual Machine in
a subprocess and connect to it, ``False`` to connect to an
existing Py4J enabled JVM
:param gateway: either a
:class:`~py4j.java_gateway.GatewayParameters` object or a
Configure a Py4J gateway.
:param context_attr: context attribute of the gateway (if omitted, the resource name will
be used instead)
:param launch_jvm: ``True`` to spawn a Java Virtual Machine in a subprocess and connect to
it, ``False`` to connect to an existing Py4J enabled JVM
:param gateway: either a :class:`~py4j.java_gateway.GatewayParameters` object or a
dictionary of keyword arguments for it
:param callback_server: callback server parameters or a boolean
indicating if a callback server is wanted
:param callback_server: callback server parameters or a boolean indicating if a callback
server is wanted
:param javaopts: options passed to Java itself
:param classpath: path or iterable of paths to pass to the JVM
launcher as the class path
:param classpath: path or iterable of paths to pass to the JVM launcher as the class path
"""
assert check_argument_types()
context_attr = context_attr or resource_name
classpath = classpath if isinstance(classpath, str) else os.pathsep.join(classpath)
javaopts = list(javaopts)

# Substitute package names with their absolute directory paths
for match in cls.package_re.finditer(classpath):
for match in package_re.finditer(classpath):
pkgname = match.group(1)
module = import_module(pkgname)
module_dir = os.path.dirname(module.__file__)
Expand All @@ -91,29 +86,27 @@ def create_gateway(cls, resource_name: str, context_attr: str=None, launch_jvm:
elif callback_server is True:
callback_server = CallbackServerParameters()

return (resource_name, context_attr, launch_jvm, gateway, callback_server, classpath,
javaopts)
return context_attr, launch_jvm, gateway, callback_server, classpath, javaopts

@coroutine
def start(self, ctx: Context):
@staticmethod
def shutdown_gateway(event, gateway: JavaGateway, resource_name: str, shutdown_jvm: bool):
if shutdown_jvm:
gateway.shutdown()
else:
gateway.close()

logger.info('Py4J gateway (%s) shut down', resource_name)

async def start(self, ctx: Context):
for (resource_name, context_attr, launch_jvm, gateway_params, callback_server_params,
classpath, javaopts) in self.gateways:
if launch_jvm:
gateway_params.port = launch_gateway(classpath=classpath, javaopts=javaopts)

gateway = JavaGateway(gateway_parameters=gateway_params,
callback_server_parameters=callback_server_params)
ctx.add_listener('finished', self.shutdown_gateway,
args=[gateway, resource_name, launch_jvm])
yield from ctx.publish_resource(gateway, resource_name, context_attr)
ctx.finished.connect(partial(self.shutdown_gateway, gateway=gateway,
resource_name=resource_name, shutdown_jvm=launch_jvm))
ctx.publish_resource(gateway, resource_name, context_attr)
logger.info('Configured Py4J gateway (%s / ctx.%s; address=%s, port=%d)',
resource_name, context_attr, gateway_params.address, gateway_params.port)

@staticmethod
def shutdown_gateway(ctx, gateway: JavaGateway, resource_name: str, shutdown_jvm: bool):
if shutdown_jvm:
gateway.shutdown()
else:
gateway.close()

logger.info('Py4J gateway (%s) shut down', resource_name)
13 changes: 13 additions & 0 deletions conda-environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: py35
dependencies:
- pip=8.1.1=py35_0
- python=3.5.1=0
- setuptools=20.3=py35_0
- xz=5.0.5=1
- zlib=1.2.8=0
- pip:
# Sphinx 1.5a0 fork w/ PRs #2431 and #2432 merged
- git+https://github.com/agronholm/sphinx.git
- sphinx-autodoc-typehints>=1.0.5
- sphinxcontrib-asyncio>=0.2.0
- /home/docs/checkouts/readthedocs.org/user_builds/asphalt-py4j/checkouts/latest
8 changes: 5 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx_autodoc_typehints'
'sphinx_autodoc_typehints',
'sphinxcontrib.asyncio'
]

templates_path = ['_templates']
Expand All @@ -15,19 +16,20 @@
author = 'Alex Grönholm'
copyright = '2015, ' + author

v = pkg_resources.get_distribution('asphalt-py4j').parsed_version
v = pkg_resources.get_distribution(project).parsed_version
version = v.base_version
release = v.public

language = None

exclude_patterns = ['_build']
pygments_style = 'sphinx'
highlight_language = 'python3'
todo_include_todos = False

html_theme = 'classic'
html_static_path = ['_static']
htmlhelp_basename = 'asphaltpy4jdoc'
htmlhelp_basename = project.replace('-', '') + 'doc'

intersphinx_mapping = {'python': ('http://docs.python.org/3/', None),
'asphalt': ('http://asphalt.readthedocs.org/en/latest/', None),
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The minimal configuration is as follows:
.. code-block:: yaml
components:
py4j: {}
py4j:
This will publish a resource of type :class:`py4j.java_gateway.JavaGateway`, named ``default``.
It will appear in the context as the ``java`` attribute.
Expand Down
6 changes: 5 additions & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
sphinx-autodoc-typehints >= 1.0.3
# Sphinx 1.5a0 fork w/ PRs #2431 and #2432 merged
git+https://github.com/agronholm/sphinx.git

sphinx-autodoc-typehints >= 1.0.5
sphinxcontrib-asyncio >= 0.2.0
19 changes: 11 additions & 8 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ Using the Java gateway
Given the inherently synchronous nature of the Py4J API, it is strongly recommended that all code
using Java gateways is run in a thread pool. The :class:`~py4j.java_gateway.JavaGateway` class is
not wrapped in any way by the component so slow calls to any Java API will block the event loop if
not run in a thread pool.
not run in a worker thread.

The following is a simple example that writes the text "Hello, Python!" to a file at
``/tmp/test.txt``. More examples can be found in the ``examples`` directory of the source
distribution.

.. code-block:: python
.. code-block:: python3
@blocking
def handler(ctx):
f = ctx.java.jvm.java.io.File('/tmp/test.txt')
writer = ctx.java.jvm.java.io.FileWriter(f)
writer.write('Hello, Python!')
writer.close()
from asyncio_extras import threadpool
async def handler(ctx):
async with threadpool():
f = ctx.java.jvm.java.io.File('/tmp/test.txt')
writer = ctx.java.jvm.java.io.FileWriter(f)
writer.write('Hello, Python!')
writer.close()
18 changes: 11 additions & 7 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
Version history
===============

1.1.0
-----
This library adheres to `Semantic Versioning <http://semver.org/>`_.

**2.0.0**

- **BACKWARD INCOMPATIBLE** Migrated to Asphalt 2.0
- Allowed combining ``gateways`` with default parameters

**1.1.0** (2016-01-02)

- Added typeguard checks to fail early if arguments of wrong types are passed to functions

1.0.1
-----
**1.0.1** (2015-11-20)

- Fixed the Asphalt dependency specification to work with setuptools older than 8.0

1.0.0
-----
**1.0.0** (2015-05-16)

Initial release.
- Initial release
37 changes: 19 additions & 18 deletions examples/simple.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
"""
A simple example that reads its own source code using Java classes and then
prints it on standard output.
A simple example that reads its own source code using Java classes and then prints it on standard
output.
"""

from asphalt.core.component import ContainerComponent
from asphalt.core.context import Context
from asphalt.core.runner import run_application
from asphalt.core.concurrency import blocking, stop_event_loop
import asyncio

from asyncio_extras.threads import threadpool
from asphalt.core import ContainerComponent, Context, run_application

class SourcePrinterComponent(ContainerComponent):
@blocking
def start(self, ctx: Context):

class ApplicationComponent(ContainerComponent):
async def start(self, ctx: Context):
self.add_component('py4j')
super().start(ctx)
await super().start(ctx)

async with threadpool():
f = ctx.java.jvm.java.io.File(__file__)
buffer = ctx.java.new_array(ctx.java.jvm.char, f.length())
reader = ctx.java.jvm.java.io.FileReader(f)
reader.read(buffer)
reader.close()
print(ctx.java.jvm.java.lang.String(buffer))

f = ctx.java.jvm.java.io.File(__file__)
buffer = ctx.java.new_array(ctx.java.jvm.char, f.length())
reader = ctx.java.jvm.java.io.FileReader(f)
reader.read(buffer)
reader.close()
print(ctx.java.jvm.java.lang.String(buffer))
stop_event_loop()
asyncio.get_event_loop().stop()

run_application(SourcePrinterComponent())
run_application(ApplicationComponent())
2 changes: 2 additions & 0 deletions readthedocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
conda:
file: conda-environment.yml
5 changes: 3 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ source-dir = docs
build-dir = docs/_build

[pytest]
addopts = -rsx --tb=short --cov
norecursedirs = .git .tox .cache build docs
addopts = -rsx --cov --tb=short
testpaths = tests

[flake8]
max-line-length = 99
exclude = .tox,docs
ignore = E251

0 comments on commit 827ee5f

Please sign in to comment.