Skip to content

Commit e0e0ca9

Browse files
committedDec 14, 2023
Merge branch 'master' into ci_jdk_matrix
2 parents f692203 + 283b39f commit e0e0ca9

File tree

7 files changed

+175
-105
lines changed

7 files changed

+175
-105
lines changed
 

‎doc/install.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Java
3434

3535
JPype source distribution includes a copy of the Java JNI header
3636
and precompiled Java code, thus the Java Development Kit (JDK) is not required.
37-
JPype has been tested with Java versions from Java 1.7 to Java 13.
37+
JPype has been tested with Java versions from Java 1.8 to Java 13.
3838

3939
C++
4040
A C++ compiler which matches the ABI used to build CPython.

‎doc/userguide.rst

+8-8
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ hard at work on your latest project but you just need to pip in the database
3636
driver for your customers database and you can call it a night. Unfortunately,
3737
it appears that your customers database will not connect to the Python database
3838
API. The whole thing is custom and the customer isn't going to supply you with
39-
a Python version. They did sent you a Java driver for the database but fat
39+
a Python version. They did send you a Java driver for the database but fat
4040
lot of good that will do for you.
4141

4242
Stumbling through the internet you find a module that says it can natively
43-
load Java packages as Python modules. Well, it worth a shot...
43+
load Java packages as Python modules. Well, it's worth a shot...
4444

4545
So first thing the guide says is that you need to install Java and set up
4646
a ``JAVA_HOME`` environment variable pointing to the JRE. Then start the
@@ -305,9 +305,9 @@ design goals.
305305
- Favor clarity over performance. This doesn't mean not trying to optimize
306306
paths, but just as premature optimization is the bane of programmers,
307307
requiring writing to maximize speed is a poor long term choice, especially
308-
in a language such as Python were weak typing can promote bit rot.
308+
in a language such as Python where weak typing can promote bit rot.
309309
310-
- If a new method has to be introduced, make look familiar.
310+
- If a new method has to be introduced, make it look familiar.
311311
Java programmers look to a method named "of" to convert to a type on
312312
factories such as a Stream, thus ``JArray.of`` converts a Python NumPy array
313313
to Java. Python programmers expect that memory backed objects can be converted
@@ -546,7 +546,7 @@ JPype Concepts
546546
***************
547547
548548
At its heart, JPype is about providing a bridge to use Java within Python.
549-
Depending on your prospective that can either be a means of accessing Java
549+
Depending on your perspective that can either be a means of accessing Java
550550
libraries from within Python or a way to use Java using Python syntax for
551551
interactivity and visualization. This mean not only exposing a limited API but
552552
instead trying to provide the entirety of the Java language with Python.
@@ -2179,7 +2179,7 @@ NumPy arrays, and conversion of NumPy integer types to Java boxed types.
21792179
Transfers to Java
21802180
=================
21812181
2182-
Memory from a NumPy array can be transferred Java in bulk. The transfer of
2182+
Memory from a NumPy array can be transferred to Java in bulk. The transfer of
21832183
a one dimensional NumPy array to Java can either be done at initialization
21842184
or by use of the Python slice operator.
21852185
@@ -2271,7 +2271,7 @@ all buffers become invalid and any access to NumPy arrays backed by Java
22712271
risk crashing. To avoid this fate, either create the memory for the buffer from
22722272
within Python and pass it to Java. Or use the Java ``java.lang.Runtime.exit``
22732273
which will terminate both the Java and Python process without leaving any
2274-
opertunity to access a dangling buffer.
2274+
opportunity to access a dangling buffer.
22752275
22762276
Buffer backed memory is not limited to use with NumPy. Buffer transfers are
22772277
supported to provide shared memory between processes or memory mapped files.
@@ -2282,7 +2282,7 @@ NumPy Primitives
22822282
================
22832283
22842284
When converting a Python type to a boxed Java type, there is the difficulty
2285-
that Java has no way to known the size of a Python numerical value. But when
2285+
that Java has no way to know the size of a Python numerical value. But when
22862286
converting NumPy numerical types, this is not an issue. The following
22872287
conversions apply to NumPy primitive types.
22882288

‎jpype/_classpath.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#
1717
# *****************************************************************************
1818
import os as _os
19+
import typing
20+
1921
import _jpype
2022

2123
__all__ = ['addClassPath', 'getClassPath']
@@ -24,7 +26,7 @@
2426
_SEP = _os.path.pathsep
2527

2628

27-
def addClassPath(path1):
29+
def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None:
2830
""" Add a path to the Java class path
2931
3032
Classpath items can be a java, a directory, or a
@@ -66,7 +68,7 @@ def addClassPath(path1):
6668
_CLASSPATHS.append(path1)
6769

6870

69-
def getClassPath(env=True):
71+
def getClassPath(env: bool = True) -> str:
7072
""" Get the full Java class path.
7173
7274
Includes user added paths and the environment CLASSPATH.
@@ -79,7 +81,7 @@ def getClassPath(env=True):
7981
global _CLASSPATHS
8082
global _SEP
8183

82-
# Merge the evironment path
84+
# Merge the environment path
8385
classPath = list(_CLASSPATHS)
8486
envPath = _os.environ.get("CLASSPATH")
8587
if env and envPath:

‎jpype/_core.py

+76-52
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
# See NOTICE file for details.
1616
#
1717
# *****************************************************************************
18-
import sys
18+
from __future__ import annotations
19+
1920
import atexit
21+
import os
22+
import sys
23+
import typing
24+
2025
import _jpype
2126
from . import types as _jtypes
2227
from . import _classpath
@@ -53,6 +58,10 @@ class JVMNotRunning(RuntimeError):
5358
pass
5459

5560

61+
if typing.TYPE_CHECKING:
62+
_PathOrStr = typing.Union[str, os.PathLike]
63+
64+
5665
# See http://scottlobdell.me/2015/04/decorators-arguments-python/
5766
def deprecated(*args):
5867
""" Marks a function a deprecated when used as decorator.
@@ -96,23 +105,48 @@ def isJVMStarted():
96105
return _jpype.isStarted()
97106

98107

99-
def _hasClassPath(args):
108+
def _hasClassPath(args: typing.Tuple[_PathOrStr, ...]) -> bool:
100109
for i in args:
101-
if i.startswith('-Djava.class.path'):
110+
if isinstance(i, str) and i.startswith('-Djava.class.path'):
102111
return True
103112
return False
104113

105114

106-
def _handleClassPath(clsList):
115+
def _handleClassPath(
116+
classpath: typing.Union[
117+
_PathOrStr,
118+
typing.Tuple[_PathOrStr, ...]
119+
],
120+
) -> str:
121+
"""
122+
Return a classpath which represents the given tuple of classpath specifications
123+
"""
107124
out = []
108-
for s in clsList:
109-
if not isinstance(s, str):
110-
raise TypeError("Classpath elements must be strings")
111-
if s.endswith('*'):
125+
126+
if isinstance(classpath, (str, os.PathLike)):
127+
classpath = (classpath,)
128+
try:
129+
# Convert anything iterable into a tuple.
130+
classpath = tuple(classpath)
131+
except TypeError:
132+
raise TypeError("Unknown class path element")
133+
134+
for element in classpath:
135+
try:
136+
pth = os.fspath(element)
137+
except TypeError as err:
138+
raise TypeError("Classpath elements must be strings or Path-like") from err
139+
140+
if isinstance(pth, bytes):
141+
# In the future we may allow this to support Paths which are undecodable.
142+
# https://docs.python.org/3/howto/unicode.html#unicode-filenames.
143+
raise TypeError("Classpath elements must be strings or Path-like")
144+
145+
if pth.endswith('*'):
112146
import glob
113-
out.extend(glob.glob(s + '.jar'))
147+
out.extend(glob.glob(pth + '.jar'))
114148
else:
115-
out.append(s)
149+
out.append(pth)
116150
return _classpath._SEP.join(out)
117151

118152

@@ -123,7 +157,14 @@ def interactive():
123157
return bool(getattr(sys, 'ps1', sys.flags.interactive))
124158

125159

126-
def startJVM(*args, **kwargs):
160+
def startJVM(
161+
*jvmargs: str,
162+
jvmpath: typing.Optional[_PathOrStr] = None,
163+
classpath: typing.Optional[typing.Sequence[_PathOrStr], _PathOrStr] = None,
164+
ignoreUnrecognized: bool = False,
165+
convertStrings: bool = False,
166+
interrupt: bool = not interactive(),
167+
) -> None:
127168
"""
128169
Starts a Java Virtual Machine. Without options it will start
129170
the JVM with the default classpath and jvmpath.
@@ -132,14 +173,14 @@ def startJVM(*args, **kwargs):
132173
The default jvmpath is determined by ``jpype.getDefaultJVMPath()``.
133174
134175
Parameters:
135-
*args (Optional, str[]): Arguments to give to the JVM.
136-
The first argument may be the path the JVM.
176+
*jvmargs (Optional, str[]): Arguments to give to the JVM.
177+
The first argument may be the path to the JVM.
137178
138179
Keyword Arguments:
139-
jvmpath (str): Path to the jvm library file,
180+
jvmpath (str, PathLike): Path to the jvm library file,
140181
Typically one of (``libjvm.so``, ``jvm.dll``, ...)
141182
Using None will apply the default jvmpath.
142-
classpath (str,[str]): Set the classpath for the JVM.
183+
classpath (str, PathLike, [str, PathLike]): Set the classpath for the JVM.
143184
This will override any classpath supplied in the arguments
144185
list. A value of None will give no classpath to JVM.
145186
ignoreUnrecognized (bool): Option to ignore
@@ -158,8 +199,7 @@ def startJVM(*args, **kwargs):
158199
159200
Raises:
160201
OSError: if the JVM cannot be started or is already running.
161-
TypeError: if an invalid keyword argument is supplied
162-
or a keyword argument conflicts with the arguments.
202+
TypeError: if a keyword argument conflicts with the positional arguments.
163203
164204
"""
165205
if _jpype.isStarted():
@@ -168,51 +208,36 @@ def startJVM(*args, **kwargs):
168208
if _JVM_started:
169209
raise OSError('JVM cannot be restarted')
170210

171-
args = list(args)
172-
173211
# JVM path
174-
jvmpath = None
175-
if args:
212+
if jvmargs:
176213
# jvm is the first argument the first argument is a path or None
177-
if not args[0] or not args[0].startswith('-'):
178-
jvmpath = args.pop(0)
179-
if 'jvmpath' in kwargs:
180-
if jvmpath:
181-
raise TypeError('jvmpath specified twice')
182-
jvmpath = kwargs.pop('jvmpath')
214+
if jvmargs[0] is None or (isinstance(jvmargs[0], str) and not jvmargs[0].startswith('-')):
215+
if jvmpath:
216+
raise TypeError('jvmpath specified twice')
217+
jvmpath = jvmargs[0]
218+
jvmargs = jvmargs[1:]
219+
183220
if not jvmpath:
184221
jvmpath = getDefaultJVMPath()
222+
else:
223+
# Allow the path to be a PathLike.
224+
jvmpath = os.fspath(jvmpath)
225+
226+
extra_jvm_args: typing.Tuple[str, ...] = tuple()
185227

186228
# Classpath handling
187-
if _hasClassPath(args):
229+
if _hasClassPath(jvmargs):
188230
# Old style, specified in the arguments
189-
if 'classpath' in kwargs:
231+
if classpath is not None:
190232
# Cannot apply both styles, conflict
191233
raise TypeError('classpath specified twice')
192-
classpath = None
193-
elif 'classpath' in kwargs:
194-
# New style, as a keywork
195-
classpath = kwargs.pop('classpath')
196-
else:
197-
# Not speficied at all, use the default classpath
234+
elif classpath is None:
235+
# Not specified at all, use the default classpath.
198236
classpath = _classpath.getClassPath()
199237

200238
# Handle strings and list of strings.
201239
if classpath:
202-
if isinstance(classpath, str):
203-
args.append('-Djava.class.path=%s' % _handleClassPath([classpath]))
204-
elif hasattr(classpath, '__iter__'):
205-
args.append('-Djava.class.path=%s' % _handleClassPath(classpath))
206-
else:
207-
raise TypeError("Unknown class path element")
208-
209-
ignoreUnrecognized = kwargs.pop('ignoreUnrecognized', False)
210-
convertStrings = kwargs.pop('convertStrings', False)
211-
interrupt = kwargs.pop('interrupt', not interactive())
212-
213-
if kwargs:
214-
raise TypeError("startJVM() got an unexpected keyword argument '%s'"
215-
% (','.join([str(i) for i in kwargs])))
240+
extra_jvm_args += (f'-Djava.class.path={_handleClassPath(classpath)}', )
216241

217242
try:
218243
import locale
@@ -221,7 +246,7 @@ def startJVM(*args, **kwargs):
221246
# Keep the current locale settings, else Java will replace them.
222247
prior = [locale.getlocale(i) for i in categories]
223248
# Start the JVM
224-
_jpype.startup(jvmpath, tuple(args),
249+
_jpype.startup(jvmpath, jvmargs + extra_jvm_args,
225250
ignoreUnrecognized, convertStrings, interrupt)
226251
# Collect required resources for operation
227252
initializeResources()
@@ -238,8 +263,7 @@ def startJVM(*args, **kwargs):
238263
match = re.search(r"([0-9]+)\.[0-9]+", source)
239264
if match:
240265
version = int(match.group(1)) - 44
241-
raise RuntimeError("%s is older than required Java version %d" % (
242-
jvmpath, version)) from ex
266+
raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex
243267
raise
244268

245269

‎jpype/_core.pyi

-2
This file was deleted.

‎native/jni_include/jni.h

+5
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,11 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);
11791179
#define JNI_VERSION_1_6 0x00010006
11801180
#define JNI_VERSION_1_7 0x00010007
11811181
#define JNI_VERSION_1_8 0x00010008
1182+
#define JNI_VERSION_9 0x00090000
1183+
#define JNI_VERSION_10 0x000a0000
1184+
#define JNI_VERSION_19 0x00130000
1185+
#define JNI_VERSION_20 0x00140000
1186+
#define JNI_VERSION_21 0x00150000
11821187

11831188
#define JNI_OK (0) /* no error */
11841189
#define JNI_ERR (-1) /* generic error */

‎test/jpypetest/test_startup.py

+80-39
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,12 @@
1616
#
1717
# *****************************************************************************
1818
import jpype
19-
import common
2019
import subrun
20+
import functools
2121
import os
22-
import sys
22+
from pathlib import Path
2323
import unittest
2424

25-
26-
def runStartJVM(*args, **kwargs):
27-
jpype.startJVM(*args, **kwargs)
28-
29-
30-
def runStartJVMTest(*args, **kwargs):
31-
jpype.startJVM(*args, **kwargs)
32-
try:
33-
jclass = jpype.JClass('jpype.array.TestArray')
34-
return
35-
except:
36-
pass
37-
raise RuntimeError("Test class not found")
38-
39-
4025
root = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
4126
cp = os.path.join(root, 'classes').replace('\\', '/')
4227

@@ -57,51 +42,107 @@ def testRestart(self):
5742
jpype.shutdownJVM()
5843
jpype.startJVM(convertStrings=False)
5944

60-
def testJVMPathKeyword(self):
61-
runStartJVM(jvmpath=self.jvmpath)
62-
6345
def testInvalidArgsFalse(self):
6446
with self.assertRaises(RuntimeError):
65-
runStartJVM("-for_sure_InVaLiD",
66-
ignoreUnrecognized=False, convertStrings=False)
47+
jpype.startJVM(
48+
"-for_sure_InVaLiD",
49+
ignoreUnrecognized=False, convertStrings=False,
50+
)
6751

6852
def testInvalidArgsTrue(self):
69-
runStartJVM("-for_sure_InVaLiD",
70-
ignoreUnrecognized=True, convertStrings=False)
53+
jpype.startJVM(
54+
"-for_sure_InVaLiD",
55+
ignoreUnrecognized=True,
56+
convertStrings=False,
57+
)
7158

7259
def testClasspathArgKeyword(self):
73-
runStartJVMTest(classpath=cp, convertStrings=False)
60+
jpype.startJVM(classpath=cp, convertStrings=False)
61+
assert jpype.JClass('jpype.array.TestArray') is not None
7462

7563
def testClasspathArgList(self):
76-
runStartJVMTest(classpath=[cp], convertStrings=False)
64+
jpype.startJVM(
65+
classpath=[cp],
66+
convertStrings=False,
67+
)
68+
assert jpype.JClass('jpype.array.TestArray') is not None
7769

7870
def testClasspathArgListEmpty(self):
79-
runStartJVMTest(classpath=[cp, ''], convertStrings=False)
71+
jpype.startJVM(
72+
classpath=[cp, ''],
73+
convertStrings=False,
74+
)
75+
assert jpype.JClass('jpype.array.TestArray') is not None
8076

8177
def testClasspathArgDef(self):
82-
runStartJVMTest('-Djava.class.path=%s' % cp, convertStrings=False)
78+
jpype.startJVM('-Djava.class.path=%s' % cp, convertStrings=False)
79+
assert jpype.JClass('jpype.array.TestArray') is not None
80+
81+
def testClasspathArgPath(self):
82+
jpype.startJVM(classpath=Path(cp), convertStrings=False)
83+
assert jpype.JClass('jpype.array.TestArray') is not None
84+
85+
def testClasspathArgPathList(self):
86+
jpype.startJVM(classpath=[Path(cp)], convertStrings=False)
87+
assert jpype.JClass('jpype.array.TestArray') is not None
88+
89+
def testClasspathArgGlob(self):
90+
jpype.startJVM(classpath=os.path.join(cp, '..', 'jar', 'mrjar*'))
91+
assert jpype.JClass('org.jpype.mrjar.A') is not None
8392

8493
def testClasspathTwice(self):
8594
with self.assertRaises(TypeError):
86-
runStartJVMTest('-Djava.class.path=%s' %
95+
jpype.startJVM('-Djava.class.path=%s' %
8796
cp, classpath=cp, convertStrings=False)
8897

8998
def testClasspathBadType(self):
9099
with self.assertRaises(TypeError):
91-
runStartJVMTest(classpath=1, convertStrings=False)
92-
93-
def testPathArg(self):
94-
runStartJVMTest(self.jvmpath, classpath=cp, convertStrings=False)
95-
96-
def testPathKeyword(self):
97-
path = jpype.getDefaultJVMPath()
98-
runStartJVMTest(classpath=cp, jvmpath=self.jvmpath,
99-
convertStrings=False)
100+
jpype.startJVM(classpath=1, convertStrings=False)
101+
102+
def testJVMPathArg_Str(self):
103+
jpype.startJVM(self.jvmpath, classpath=cp, convertStrings=False)
104+
assert jpype.JClass('jpype.array.TestArray') is not None
105+
106+
def testJVMPathArg_None(self):
107+
# It is allowed to pass None as a JVM path
108+
jpype.startJVM(
109+
None, # type: ignore
110+
classpath=cp,
111+
)
112+
assert jpype.JClass('jpype.array.TestArray') is not None
113+
114+
def testJVMPathArg_NoArgs(self):
115+
jpype.startJVM(
116+
classpath=cp,
117+
)
118+
assert jpype.JClass('jpype.array.TestArray') is not None
119+
120+
def testJVMPathArg_Path(self):
121+
with self.assertRaises(TypeError):
122+
jpype.startJVM(
123+
# Pass a path as the first argument. This isn't supported (this is
124+
# reflected in the type definition), but the fact that it "works"
125+
# gives rise to this test.
126+
Path(self.jvmpath), # type: ignore
127+
convertStrings=False,
128+
)
129+
130+
def testJVMPathKeyword_str(self):
131+
jpype.startJVM(
132+
classpath=cp,
133+
jvmpath=self.jvmpath,
134+
convertStrings=False,
135+
)
136+
assert jpype.JClass('jpype.array.TestArray') is not None
137+
138+
def testJVMPathKeyword_Path(self):
139+
jpype.startJVM(jvmpath=Path(self.jvmpath), classpath=cp, convertStrings=False)
140+
assert jpype.JClass('jpype.array.TestArray') is not None
100141

101142
def testPathTwice(self):
102143
with self.assertRaises(TypeError):
103144
jpype.startJVM(self.jvmpath, jvmpath=self.jvmpath)
104145

105146
def testBadKeyword(self):
106147
with self.assertRaises(TypeError):
107-
jpype.startJVM(invalid=True)
148+
jpype.startJVM(invalid=True) # type: ignore

0 commit comments

Comments
 (0)
Please sign in to comment.