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

Module #1249

Open
wants to merge 43 commits into
base: master
Choose a base branch
from
Open

Module #1249

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
eacc1fe
Work on module support
Thrameos Nov 27, 2024
3ef37ba
Remove agent logic trying to resolve non ascii
Thrameos Nov 27, 2024
4766f12
Cleanup and work for nonascii friends
Thrameos Nov 27, 2024
6387988
Ready for merge
Thrameos Nov 27, 2024
95346dc
Merge remote-tracking branch 'refs/remotes/origin/no_agent' into no_a…
Thrameos Nov 27, 2024
6950c29
Fixed bug in the order of operations
Thrameos Nov 27, 2024
cc63e92
Fix warning
Thrameos Nov 28, 2024
df0f73b
No need to make an extra class loader.
Thrameos Nov 28, 2024
f828f80
Push to test removal
Thrameos Nov 28, 2024
1265979
Merge remote-tracking branch 'refs/remotes/origin/no_agent' into no_a…
Thrameos Nov 28, 2024
70cc3c1
Merging from main
Thrameos Nov 28, 2024
a0b2b3e
Merge remote-tracking branch 'refs/remotes/origin/no_agent' into no_a…
Thrameos Nov 28, 2024
94f21a1
Remove the need for late load... Everyone is equal!
Thrameos Nov 28, 2024
86c7878
Jar support for jpype.class.path
Thrameos Nov 28, 2024
a7197fb
Cleanup
Thrameos Nov 28, 2024
c8ed839
Working on debugging dbapi2 and forName
Thrameos Nov 28, 2024
d5c18dc
Merge remote-tracking branch 'refs/remotes/origin/no_agent' into no_a…
Thrameos Nov 28, 2024
4e63379
Still working on Database services
Thrameos Nov 28, 2024
4344295
Rework to use reflector class
Thrameos Nov 28, 2024
3cefadd
Why must we specify stuff on our private methods?
Thrameos Nov 28, 2024
3703760
Shut up mypy
Thrameos Nov 28, 2024
ff12201
Update build process to include reflector
Thrameos Nov 28, 2024
4f1eb89
Revised some tests which don't run properly in non-ascii root
Thrameos Nov 28, 2024
7ae5ce8
Merge remote-tracking branch 'refs/remotes/origin/no_agent' into no_a…
Thrameos Nov 28, 2024
fb94b98
Remove agent option for now
Thrameos Nov 28, 2024
9223000
Works on nonascii path
Thrameos Nov 29, 2024
81f5687
Start clean up
Thrameos Nov 29, 2024
b657fbe
Cleanup
Thrameos Nov 29, 2024
3844c8c
Whitespace
Thrameos Nov 29, 2024
25a2408
Jdk 1.8 build failed. Try again
Thrameos Nov 29, 2024
3942798
Another fix for (broken) Windows
Thrameos Nov 29, 2024
9b15bd8
Fix setupext for JDK 8
Thrameos Nov 29, 2024
3a5d118
Cleanup pass
Thrameos Nov 29, 2024
f38b427
Cleanup and test
Thrameos Nov 29, 2024
b54514c
Merge remote-tracking branch 'refs/remotes/origin/no_agent' into no_a…
Thrameos Nov 29, 2024
3764749
Try again for delete.
Thrameos Nov 29, 2024
67f732d
Fix broken test
Thrameos Nov 29, 2024
e95010f
Cleanup and doc
Thrameos Nov 30, 2024
800ce78
Merge remote-tracking branch 'refs/remotes/origin/no_agent' into no_a…
Thrameos Nov 30, 2024
fad418d
Work on module support
Thrameos Dec 2, 2024
b932bad
Removing bean access as it requires java.management which is an extra…
Thrameos Dec 2, 2024
773ec40
Working on loading into module
Thrameos Dec 2, 2024
13107ae
Work on clean up and build
Thrameos Dec 2, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ jacoco/
wheelhouse/
vc*.pdb
*.class
/project/jpype_java_old/nbproject/private/
5 changes: 5 additions & 0 deletions doc/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ This changelog *only* contains changes from the *first* pypi release (0.5.4.3) o

Latest Changes:
- **1.5.2.dev0 - 2024-11-18**

- Roll back agent change due to misbehaving JVM installs.

- Correct issues with non-ascii path for jdbc connections and forName.

- **1.5.1 - 2024-11-09**

- Future proofing for Python 3.14
Expand Down
6 changes: 3 additions & 3 deletions jpype/_classpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None:
path1 = path2.joinpath(path1)

# If the JVM is already started then we will have to load the paths
# immediately into the DynamicClassLoader
# immediately into the JPypeClassLoader
if _jpype.isStarted():
Paths = _jpype.JClass('java.nio.file.Paths')
JContext = _jpype.JClass('org.jpype.JPypeContext')
Expand All @@ -62,9 +62,9 @@ def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None:
if len(paths) == 0:
return
for path in paths:
classLoader.addFile(Paths.get(str(path)))
classLoader.addPath(Paths.get(str(path)))
else:
classLoader.addFile(Paths.get(str(path1)))
classLoader.addPath(Paths.get(str(path1)))
_CLASSPATHS.append(path1)


Expand Down
205 changes: 128 additions & 77 deletions jpype/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,30 +109,29 @@
return _jpype.isStarted()


def _getOldClassPath(args) -> list[str]:
for i in args:
if not isinstance(i, str):
def _getOption(args, var, sep=None, keep=False):
""" Get an option and remove it from the current jvm arguments list """
for i,v in enumerate(args):
if not isinstance(v, str):
continue
_, _, classpath = i.partition('-Djava.class.path=')
if classpath:
return classpath.split(_classpath._SEP)
_, _, value = v.partition('%s='%var)
if value:
if not keep:
del args[i]
if sep is not None:
return value.split(sep)
return value
return []


def _hasSystemClassLoader(args) -> bool:
for i in args:
if isinstance(i, str) and i.startswith('-Djava.system.class.loader'):
return True
return False


def _handleClassPath(
def _expandClassPath(
classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None
) -> typing.Sequence[str]:
) -> typing.List[str]:
"""
Return a classpath which represents the given tuple of classpath specifications
"""
out: list[str] = []
if classpath is None:
return out
if isinstance(classpath, (str, os.PathLike)):
classpath = (classpath,)
try:
Expand Down Expand Up @@ -160,21 +159,45 @@
return out


def _removeClassPath(args) -> tuple[str]:
return tuple(arg for arg in args if not str(arg).startswith("-Djava.class.path"))


_JVM_started = False


def interactive():
return bool(getattr(sys, 'ps1', sys.flags.interactive))

def _findTemp():
dirlist = []
# Mirror Python tempfile with a check for ascii
for envname in 'TMPDIR', 'TEMP', 'TMP':
dirname = os.getenv(envname)
if dirname and dirname.isascii():
dirlist.append(dirname)
if os.name == 'nt':
for dirname in [ os.path.expanduser(r'~\AppData\Local\Temp'),
os.path.expandvars(r'%SYSTEMROOT%\Temp'),
r'c:\temp', r'c:\tmp', r'\temp', r'\tmp' ]:
if dirname and dirname.isascii():
dirlist.append(dirname)
else:
dirlist.extend([ '/tmp', '/var/tmp', '/usr/tmp' ])

name = str(os.getpid())
for d in dirlist:
p = Path("%s/%s"%(d,name))
try:
p.touch()
p.unlink()
except Exception as ex:
continue
return d
raise SystemError("Unable to find non-ansii path")


def startJVM(
*jvmargs: str,
jvmpath: typing.Optional[_PathOrStr] = None,
classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None,
modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None,
modules: typing.Union[typing.Sequence[str], str, None] = None,
ignoreUnrecognized: bool = False,
convertStrings: bool = False,
interrupt: bool = not interactive(),
Expand All @@ -197,6 +220,9 @@
classpath (str, PathLike, [str, PathLike]): Set the classpath for the JVM.
This will override any classpath supplied in the arguments
list. A value of None will give no classpath to JVM.
modulepath (str, PathLike, [str, PathLike]): Set the modulepath for the JVM.
This will override any modulepath supplied in the arguments
list. A value of None will give no classpath to JVM.
ignoreUnrecognized (bool): Option to ignore
invalid JVM arguments. Default is False.
convertStrings (bool): Option to force Java strings to
Expand All @@ -216,32 +242,57 @@
TypeError: if a keyword argument conflicts with the positional arguments.

"""

# Code for 1.6
# modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None,

if _jpype.isStarted():
raise OSError('JVM is already started')
global _JVM_started
if _JVM_started:
raise OSError('JVM cannot be restarted')

has_classloader = _hasSystemClassLoader(jvmargs)

# Convert to list
jvm_args: list[str] = list(jvmargs)

# JVM path
if jvmargs:
if jvm_args:
# jvm is the first argument the first argument is a path or None
if jvmargs[0] is None or (isinstance(jvmargs[0], str) and not jvmargs[0].startswith('-')):
if jvm_args[0] is None or (isinstance(jvm_args[0], str) and not jvm_args[0].startswith('-')):
if jvmpath:
raise TypeError('jvmpath specified twice')
jvmpath = jvmargs[0]
jvmargs = jvmargs[1:]
jvmpath = jvm_args[0]
jvm_args = jvm_args[1:]

if not jvmpath:
jvmpath = getDefaultJVMPath()
else:
# Allow the path to be a PathLike.
jvmpath = os.fspath(jvmpath)

# Handle strings and list of strings.
extra_jvm_args: list[str] = []

# Get the support library
support_lib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar")
if not os.path.exists(support_lib):
raise RuntimeError("Unable to find org.jpype.jar support library at " + support_lib)

# If we are not installed on an ascii path then we will need to copy the jar to a new location
if not support_lib.isascii():
import tempfile
import shutil
fd, path = tempfile.mkstemp(dir = _findTemp())
if not path.isascii():
raise ValueError("Unable to find ascii temp directory.")
shutil.copyfile(support_lib, path)
support_lib = path
tmp = path

Check warning

Code scanning / CodeQL

Variable defined multiple times Warning

This assignment to 'tmp' is unnecessary as it is
redefined
before this value is used.
os.close(fd)
# Don't remove

# Classpath handling
old_classpath = _getOldClassPath(jvmargs)
old_classpath = _getOption(jvm_args, "-Djava.class.path", _classpath._SEP)
if old_classpath:
# Old style, specified in the arguments
if classpath is not None:
Expand All @@ -252,41 +303,58 @@
# Not specified at all, use the default classpath.
classpath = _classpath.getClassPath()

# Handle strings and list of strings.
extra_jvm_args: list[str] = []
# Modulepath handling
old_modulepath = _getOption(jvm_args, "--module-path", _classpath._SEP)
if old_modulepath:
# Old style, specified in the arguments
if modulepath is not None:
# Cannot apply both styles, conflict
raise TypeError('modulepath specified twice')
modulepath = old_modulepath

supportLib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar")
if not os.path.exists(supportLib):
raise RuntimeError("Unable to find org.jpype.jar support library at " + supportLib)

late_load = not has_classloader
if classpath:
cp = _classpath._SEP.join(_handleClassPath(classpath))
if cp.isascii():
# no problems
extra_jvm_args += ['-Djava.class.path=%s'%cp ]
jvmargs = _removeClassPath(jvmargs)
late_load = False
elif has_classloader:
# Modules
old_modules = _getOption(jvm_args, "--add-modules", ',')
if old_modules:
# Old style, specified in the arguments
if modules is not None:
# Cannot apply both styles, conflict
raise TypeError('modules specified twice')
modules = old_modules
if modules is None:
modules = []
modules.append("org.jpype")

system_class_loader = _getOption(jvm_args, "-Djava.system.class.loader", keep=True)

java_class_path = _expandClassPath(classpath)
java_class_path = list(filter(len, java_class_path))
classpath = _classpath._SEP.join(java_class_path)
tmp = None

# Make sure our module is always on the classpath
if not classpath.isascii():
if system_class_loader:
# https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22
raise ValueError("system classloader cannot be specified with non ascii characters in the classpath")
elif supportLib.isascii():
# ok, setup the jpype system classloader and add to the path after startup
# this guarentees all classes have the same permissions as they did in the past
extra_jvm_args += [
'-Djava.system.class.loader=org.jpype.classloader.JpypeSystemClassLoader',
'-Djava.class.path=%s'%supportLib
]
jvmargs = _removeClassPath(jvmargs)
else:
# We are screwed no matter what we try or do.
# Unfortunately the jdk maintainers don't seem to care either.
# This bug is almost 10 years old and spans 16 jdk versions and counting.
# https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22
raise ValueError("jpype jar must be ascii to add to the system class path")

# ok, setup the jpype system classloader and add to the path after startup
# this guarentees all classes have the same permissions as they did in the past
from urllib.parse import quote
extra_jvm_args += [
'-Djava.system.class.loader=org.jpype.JPypeClassLoader',
'-Djava.class.path=%s'%support_lib,
'-Djpype.class.path=%s'%quote(classpath),
'-Xshare:off'
]
else:
# no problems
if classpath:
extra_jvm_args += ['-Djava.class.path=%s'%classpath ]

extra_jvm_args += ['-javaagent:' + supportLib]
mp =_expandClassPath(modulepath)
mp.append(support_lib)
extra_jvm_args += ['--module-path=%s'%_classpath._SEP.join(mp) ]
extra_jvm_args += ['--add-modules=%s'%(",".join(modules))]

try:
import locale
Expand All @@ -295,8 +363,8 @@
# Keep the current locale settings, else Java will replace them.
prior = [locale.getlocale(i) for i in categories]
# Start the JVM
_jpype.startup(jvmpath, jvmargs + tuple(extra_jvm_args),
ignoreUnrecognized, convertStrings, interrupt)
_jpype.startup(jvmpath, tuple(jvm_args + extra_jvm_args),
ignoreUnrecognized, convertStrings, interrupt, tmp)
# Collect required resources for operation
initializeResources()
# Restore locale
Expand All @@ -315,23 +383,6 @@
raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex
raise

"""Prior versions of JPype used the jvmargs to setup the class paths via
JNI (Java Native Interface) option strings:
i.e -Djava.class.path=...
See: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html

Unfortunately, only ascii paths work because url encoding is not handled correctly
see: https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22

To resolve this issue, we add the classpath after initialization since Java has
had the utilities to correctly encode it since 1.0
"""
if late_load and classpath:
# now we can add to the system classpath
cl = _jpype.JClass("java.lang.ClassLoader").getSystemClassLoader()
for cp in _handleClassPath(classpath):
cl.addPath(_jpype._java_lang_String(cp))


def initializeResources():
global _JVM_started
Expand Down
3 changes: 2 additions & 1 deletion jpype/dbapi2.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,9 @@ def connect(dsn, *, driver=None, driver_args=None,
"""
Properties = _jpype.JClass("java.util.Properties")
if driver:
_jpype.JClass('java.lang.Class').forName(driver).newInstance()
_jpype.JClass('java.lang.Class').forName(driver,True,_jpype.JPypeClassLoader).newInstance()
DM = _jpype.JClass('java.sql.DriverManager')
#DM.setLogStream(_jpype.JClass("java.lang.System").out)

# User is supplying Java properties
if isinstance(driver_args, Properties):
Expand Down
1 change: 1 addition & 0 deletions native/common/include/jp_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ class JPContext

private:
JPClassRef m_Array;
JPObjectRef m_Reflector;

// Java Functions
jmethodID m_Object_ToStringID{};
Expand Down
1 change: 0 additions & 1 deletion native/common/include/jp_gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ class JPGarbageCollection
jmethodID _freeMemoryID;
jmethodID _maxMemoryID;
jmethodID _usedMemoryID;
jmethodID _heapMemoryID;

size_t last_python;
size_t last_java;
Expand Down
9 changes: 8 additions & 1 deletion native/common/jp_classloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,16 @@ JPClassLoader::JPClassLoader(JPJavaFrame& frame)
m_SystemClassLoader = JPObjectRef(frame,
frame.CallStaticObjectMethodA(classLoaderClass, getSystemClassLoader, nullptr));

jclass dynamicLoaderClass = frame.getEnv()->FindClass("org/jpype/classloader/DynamicClassLoader");
jclass dynamicLoaderClass = frame.getEnv()->FindClass("org/jpype/JPypeClassLoader");
if (dynamicLoaderClass != nullptr)
{
// Use the one in place already
if (frame.IsInstanceOf(m_SystemClassLoader.get(), dynamicLoaderClass))
{
m_BootLoader = m_SystemClassLoader;
return;
}

// Easy the Dynamic loader is already in the path, so just use it as the bootloader
jmethodID newDyLoader = frame.GetMethodID(dynamicLoaderClass, "<init>",
"(Ljava/lang/ClassLoader;)V");
Expand Down
Loading
Loading