From 0f153a5442a819e21fe668e776aee529ac41f06a Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Thu, 5 May 2022 12:25:12 +0200 Subject: [PATCH] Support PathLike types for classpath and jvmpath. Closes https://github.com/jpype-project/jpype/issues/529 --- jpype/_classpath.py | 8 ++- jpype/_core.py | 128 +++++++++++++++++++-------------- test/jpypetest/test_startup.py | 58 ++++++++++----- 3 files changed, 120 insertions(+), 74 deletions(-) diff --git a/jpype/_classpath.py b/jpype/_classpath.py index 78383ed2d..2d8c919eb 100644 --- a/jpype/_classpath.py +++ b/jpype/_classpath.py @@ -16,6 +16,8 @@ # # ***************************************************************************** import os as _os +import typing + import _jpype __all__ = ['addClassPath', 'getClassPath'] @@ -24,7 +26,7 @@ _SEP = _os.path.pathsep -def addClassPath(path1): +def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None: """ Add a path to the Java class path Classpath items can be a java, a directory, or a @@ -66,7 +68,7 @@ def addClassPath(path1): _CLASSPATHS.append(path1) -def getClassPath(env=True): +def getClassPath(env: bool = True) -> str: """ Get the full Java class path. Includes user added paths and the environment CLASSPATH. @@ -79,7 +81,7 @@ def getClassPath(env=True): global _CLASSPATHS global _SEP - # Merge the evironment path + # Merge the environment path classPath = list(_CLASSPATHS) envPath = _os.environ.get("CLASSPATH") if env and envPath: diff --git a/jpype/_core.py b/jpype/_core.py index a71935535..bb634f4c6 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -15,8 +15,13 @@ # See NOTICE file for details. # # ***************************************************************************** -import sys +from __future__ import annotations + import atexit +import os +import sys +import typing + import _jpype from . import types as _jtypes from . import _classpath @@ -61,6 +66,10 @@ def versionTest(): pass +if typing.TYPE_CHECKING: + _PathOrStr = typing.Union[str, os.PathLike] + + # See http://scottlobdell.me/2015/04/decorators-arguments-python/ def deprecated(*args): """ Marks a function a deprecated when used as decorator. @@ -104,23 +113,48 @@ def isJVMStarted(): return _jpype.isStarted() -def _hasClassPath(args): +def _hasClassPath(args: typing.Tuple[_PathOrStr, ...]) -> bool: for i in args: - if i.startswith('-Djava.class.path'): + if isinstance(i, str) and i.startswith('-Djava.class.path'): return True return False -def _handleClassPath(clsList): +def _handleClassPath( + classpath: typing.Union[ + _PathOrStr, + typing.Tuple[_PathOrStr, ...] + ], +) -> str: + """ + Return a classpath which represents the given tuple of classpath specifications + """ out = [] - for s in clsList: - if not isinstance(s, str): - raise TypeError("Classpath elements must be strings") - if s.endswith('*'): + + if isinstance(classpath, (str, os.PathLike)): + classpath = (classpath,) + try: + # Convert anything iterable into a tuple. + classpath = tuple(classpath) + except TypeError: + raise TypeError("Unknown class path element") + + for element in classpath: + try: + pth = os.fspath(element) + except TypeError as err: + raise TypeError("Classpath elements must be strings or Path-like") from err + + if isinstance(pth, bytes): + # In the future we may allow this to support Paths which are undecodable. + # https://docs.python.org/3/howto/unicode.html#unicode-filenames. + raise TypeError("Classpath elements must be strings or Path-like") + + if pth.endswith('*'): import glob - out.extend(glob.glob(s + '.jar')) + out.extend(glob.glob(pth + '.jar')) else: - out.append(s) + out.append(pth) return _classpath._SEP.join(out) @@ -131,7 +165,14 @@ def interactive(): return bool(getattr(sys, 'ps1', sys.flags.interactive)) -def startJVM(*args, **kwargs): +def startJVM( + *jvmargs: str, + jvmpath: typing.Optional[_PathOrStr] = None, + classpath: typing.Optional[_PathOrStr] = None, + ignoreUnrecognized: bool = False, + convertStrings: bool = False, + interrupt: bool = not interactive(), +) -> None: """ Starts a Java Virtual Machine. Without options it will start the JVM with the default classpath and jvmpath. @@ -140,14 +181,14 @@ def startJVM(*args, **kwargs): The default jvmpath is determined by ``jpype.getDefaultJVMPath()``. Parameters: - *args (Optional, str[]): Arguments to give to the JVM. - The first argument may be the path the JVM. + *jvmargs (Optional, str[]): Arguments to give to the JVM. + The first argument may be the path to the JVM. Keyword Arguments: - jvmpath (str): Path to the jvm library file, + jvmpath (str, PathLike): Path to the jvm library file, Typically one of (``libjvm.so``, ``jvm.dll``, ...) Using None will apply the default jvmpath. - classpath (str,[str]): Set the classpath for the JVM. + 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. ignoreUnrecognized (bool): Option to ignore @@ -166,8 +207,7 @@ def startJVM(*args, **kwargs): Raises: OSError: if the JVM cannot be started or is already running. - TypeError: if an invalid keyword argument is supplied - or a keyword argument conflicts with the arguments. + TypeError: if a keyword argument conflicts with the positional arguments. """ if _jpype.isStarted(): @@ -176,54 +216,39 @@ def startJVM(*args, **kwargs): if _JVM_started: raise OSError('JVM cannot be restarted') - args = list(args) - # JVM path - jvmpath = None - if args: + if jvmargs: # jvm is the first argument the first argument is a path or None - if not args[0] or not args[0].startswith('-'): - jvmpath = args.pop(0) - if 'jvmpath' in kwargs: - if jvmpath: - raise TypeError('jvmpath specified twice') - jvmpath = kwargs.pop('jvmpath') + if jvmargs[0] is not None and isinstance(jvmargs[0], str) and not jvmargs[0].startswith('-'): + if jvmpath: + raise TypeError('jvmpath specified twice') + jvmpath = jvmargs[0] + jvmargs = jvmargs[1:] + if not jvmpath: jvmpath = getDefaultJVMPath() + else: + # Allow the path to be a PathLike. + jvmpath = os.fspath(jvmpath) + + extra_jvm_args = tuple() # Classpath handling - if _hasClassPath(args): + if _hasClassPath(jvmargs): # Old style, specified in the arguments - if 'classpath' in kwargs: + if classpath is not None: # Cannot apply both styles, conflict raise TypeError('classpath specified twice') - classpath = None - elif 'classpath' in kwargs: - # New style, as a keywork - classpath = kwargs.pop('classpath') - else: - # Not speficied at all, use the default classpath + elif classpath is None: + # Not specified at all, use the default classpath. classpath = _classpath.getClassPath() # Handle strings and list of strings. if classpath: - if isinstance(classpath, str): - args.append('-Djava.class.path=%s' % _handleClassPath([classpath])) - elif hasattr(classpath, '__iter__'): - args.append('-Djava.class.path=%s' % _handleClassPath(classpath)) - else: - raise TypeError("Unknown class path element") - - ignoreUnrecognized = kwargs.pop('ignoreUnrecognized', False) - convertStrings = kwargs.pop('convertStrings', False) - interrupt = kwargs.pop('interrupt', not interactive()) - - if kwargs: - raise TypeError("startJVM() got an unexpected keyword argument '%s'" - % (','.join([str(i) for i in kwargs]))) + extra_jvm_args += (f'-Djava.class.path={_handleClassPath(classpath)}', ) try: - _jpype.startup(jvmpath, tuple(args), + _jpype.startup(jvmpath, jvmargs + extra_jvm_args, ignoreUnrecognized, convertStrings, interrupt) initializeResources() except RuntimeError as ex: @@ -233,8 +258,7 @@ def startJVM(*args, **kwargs): match = re.search(r"([0-9]+)\.[0-9]+", source) if match: version = int(match.group(1)) - 44 - raise RuntimeError("%s is older than required Java version %d" % ( - jvmpath, version)) from ex + raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex raise diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index c64d99cbf..0dd609a9c 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -18,23 +18,20 @@ import jpype import common import subrun +import functools import os +from pathlib import Path import sys import unittest -def runStartJVM(*args, **kwargs): - jpype.startJVM(*args, **kwargs) - - +@functools.wraps(jpype.startJVM) def runStartJVMTest(*args, **kwargs): jpype.startJVM(*args, **kwargs) try: - jclass = jpype.JClass('jpype.array.TestArray') - return - except: - pass - raise RuntimeError("Test class not found") + assert jpype.JClass('jpype.array.TestArray') is not None + except Exception as err: + raise RuntimeError("Test class not found") from err root = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) @@ -57,17 +54,18 @@ def testRestart(self): jpype.shutdownJVM() jpype.startJVM(convertStrings=False) - def testJVMPathKeyword(self): - runStartJVM(jvmpath=self.jvmpath) - def testInvalidArgsFalse(self): with self.assertRaises(RuntimeError): - runStartJVM("-for_sure_InVaLiD", - ignoreUnrecognized=False, convertStrings=False) + jpype.startJVM( + "-for_sure_InVaLiD", + ignoreUnrecognized=False, convertStrings=False, + ) def testInvalidArgsTrue(self): - runStartJVM("-for_sure_InVaLiD", - ignoreUnrecognized=True, convertStrings=False) + jpype.startJVM( + "-for_sure_InVaLiD", + ignoreUnrecognized=True, convertStrings=False, + ) def testClasspathArgKeyword(self): runStartJVMTest(classpath=cp, convertStrings=False) @@ -81,6 +79,16 @@ def testClasspathArgListEmpty(self): def testClasspathArgDef(self): runStartJVMTest('-Djava.class.path=%s' % cp, convertStrings=False) + def testClasspathArgPath(self): + runStartJVMTest(classpath=Path(cp), convertStrings=False) + + def testClasspathArgPathList(self): + runStartJVMTest(classpath=[Path(cp)], convertStrings=False) + + def testClasspathArgGlob(self): + jpype.startJVM(classpath=os.path.join(cp, '..', 'jar', 'mrjar*')) + assert jpype.JClass('org.jpype.mrjar.A') is not None + def testClasspathTwice(self): with self.assertRaises(TypeError): runStartJVMTest('-Djava.class.path=%s' % @@ -90,14 +98,26 @@ def testClasspathBadType(self): with self.assertRaises(TypeError): runStartJVMTest(classpath=1, convertStrings=False) - def testPathArg(self): + def testJVMPathArg_Str(self): runStartJVMTest(self.jvmpath, classpath=cp, convertStrings=False) - def testPathKeyword(self): - path = jpype.getDefaultJVMPath() + def testJVMPathArg_Path(self): + with self.assertRaises(TypeError): + runStartJVMTest([ + # Pass a path as the first argument. This isn't supported (this is + # reflected in the type definition), but the fact that it "works" + # gives rise to this test. + Path(self.jvmpath), cp], # type: ignore + convertStrings=False, + ) + + def testJVMPathKeyword_str(self): runStartJVMTest(classpath=cp, jvmpath=self.jvmpath, convertStrings=False) + def testJVMPathKeyword_Path(self): + runStartJVMTest(jvmpath=Path(self.jvmpath), classpath=cp, convertStrings=False) + def testPathTwice(self): with self.assertRaises(TypeError): jpype.startJVM(self.jvmpath, jvmpath=self.jvmpath)