diff --git a/README.md b/README.md index e7c17314..eb22bbb8 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ FUNCTIONS This function is a shortcut for Java's System.gc(). - :raise RuntimeError: if the JVM has not yet been started. + :raise RuntimeError: If the JVM has not started yet. get_version(java_class_or_python_package) -> str Return the version of a Java class or Python package. @@ -213,13 +213,13 @@ FUNCTIONS those actions via the jpype.setupGuiEnvironment wrapper function; see the Troubleshooting section of the scyjava README for details. - is_jarray(data) -> bool + is_jarray(data: Any) -> bool Return whether the given data object is a Java array. is_jvm_headless() -> bool Return true iff Java is running in headless mode. - :raises RuntimeError: If the JVM has not started yet. + :raise RuntimeError: If the JVM has not started yet. is_memoryarraylike(arr: Any) -> bool Return True iff the object is memoryarraylike: @@ -265,7 +265,7 @@ FUNCTIONS :param lengths: List of lengths for the array. For example: `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. You can pass a single integer to make a 1-dimensional array of that length. - :returns: The newly allocated array + :return: The newly allocated array jclass(data) Obtain a Java class object. @@ -285,22 +285,23 @@ FUNCTIONS i.e. the Java class for the Class class. :-) :param data: The object from which to glean the class. - :returns: A java.lang.Class object, suitable for use with reflection. - :raises TypeError: if the argument is not one of the aforementioned types. + :return: A java.lang.Class object, suitable for use with reflection. + :raise TypeError: if the argument is not one of the aforementioned types. jimport(class_name: str) Import a class from Java to Python. :param class_name: Name of the class to import. - :returns: A pointer to the class, which can be used to - e.g. instantiate objects of that class. + :return: + A pointer to the class, which can be used to + e.g. instantiate objects of that class. jinstance(obj, jtype) -> bool Test if the given object is an instance of a particular Java type. :param obj: The object to check. :param jtype: The Java type, as either a jimported class or as a string. - :returns: True iff the object is an instance of that Java type. + :return: True iff the object is an instance of that Java type. jstacktrace(exc) -> str Extract the Java-side stack trace from a Java exception. @@ -315,7 +316,7 @@ FUNCTIONS print(jstacktrace(exc)) :param exc: The Java Throwable from which to extract the stack trace. - :returns: A multi-line string containing the stack trace, or empty string + :return: A multi-line string containing the stack trace, or empty string if no stack trace could be extracted. jvm_started() -> bool @@ -379,8 +380,18 @@ FUNCTIONS :return: The used memory in bytes. :raise RuntimeError: if the JVM has not yet been started. + numeric_bounds(the_type: type) -> Union[Tuple[int, int], Tuple[float, float], Tuple[NoneType, NoneType]] + Get the minimum and maximum values for the given numeric type. + For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), + whereas a Java double returns (float(-Double.MAX_VALUE), float(Double.MAX_VALUE)). + + :param the_type: The type whose minimum and maximum values are needed. + :return: + The minimum and maximum values as a two-element tuple of int or float, + or a two-element tuple of None if no known bounds. + shutdown_jvm() -> None - Shutdown the JVM. + Shut down the JVM. This function makes a best effort to clean up Java resources first. In particular, shutdown hooks registered with scyjava.when_jvm_stops @@ -398,7 +409,7 @@ FUNCTIONS Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. - :raises RuntimeError: if this method is called while in Jep mode. + :raise RuntimeError: if this method is called while in Jep mode. start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can @@ -407,8 +418,9 @@ FUNCTIONS time a scyjava function needing a JVM is invoked, one is started on the fly with the configuration specified via the scijava.config mechanism. - :param options: List of options to pass to the JVM. For example: - ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] + :param options: + List of options to pass to the JVM. + For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] to_java(obj: Any, **hints: Dict) -> Any Recursively convert a Python object to a Java object. @@ -451,11 +463,13 @@ FUNCTIONS * float values in Double range but outside float range convert to Double * float values outside double range convert to BigDecimal - :param obj: The Python object to convert. - :param hints: An optional dictionary of hints, to help scyjava - make decisions about how to do the conversion. - :returns: A corresponding Java object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. + :param obj: + The Python object to convert. + :param hints: + An optional dictionary of hints, to help scyjava + make decisions about how to do the conversion. + :return: A corresponding Java object with the same contents. + :raise TypeError: if the argument is not one of the aforementioned types. to_python(data: Any, gentle: bool = False) -> Any Recursively convert a Java object to a Python object. @@ -472,12 +486,15 @@ FUNCTIONS * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator - :param data: The Java object to convert. - :param gentle: If set, and the type cannot be converted, leaves - the data alone rather than raising a TypeError. - :returns: A corresponding Python object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types, - and the gentle flag is not set. + :param data: + The Java object to convert. + :param gentle: + If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. + :return: A corresponding Python object with the same contents. + :raise TypeError: + if the argument is not one of the aforementioned types, + and the gentle flag is not set. when_jvm_starts(f) -> None Registers a function to be called when the JVM starts (or immediately). diff --git a/pyproject.toml b/pyproject.toml index 4c368d59..f3065760 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ extend-ignore = ["E203"] # See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 max-line-length = 88 min_python_version = "3.8" +per-file-ignores = "__init__.py:F401" [tool.isort] profile = "black" diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 35deb874..5cd1fc77 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -70,12 +70,8 @@ from functools import lru_cache from typing import Any, Callable, Dict -from scyjava._arrays import ( # noqa: F401 - is_arraylike, - is_memoryarraylike, - is_xarraylike, -) -from scyjava._convert import ( # noqa: F401 +from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike +from ._convert import ( Converter, JavaCollection, JavaIterable, @@ -94,19 +90,12 @@ to_java, to_python, ) -from scyjava._java import ( # noqa: F401 - JavaClasses, +from ._jvm import ( # noqa: F401 available_processors, gc, is_awt_initialized, - is_jarray, is_jvm_headless, - isjava, - jarray, - jclass, jimport, - jinstance, - jstacktrace, jvm_started, jvm_version, memory_max, @@ -117,12 +106,18 @@ when_jvm_starts, when_jvm_stops, ) -from scyjava._script import enable_python_scripting # noqa: F401 -from scyjava._versions import ( # noqa: F401 - compare_version, - get_version, - is_version_at_least, +from ._script import enable_python_scripting +from ._types import ( + JavaClasses, + is_jarray, + isjava, + jarray, + jclass, + jinstance, + jstacktrace, + numeric_bounds, ) +from ._versions import compare_version, get_version, is_version_at_least __version__ = get_version("scyjava") __all__ = [ diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index fa63cc16..af1583b5 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -12,18 +12,9 @@ from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort -from ._java import ( - JavaClasses, - Mode, - is_jarray, - isjava, - jarray, - jclass, - jimport, - jinstance, - mode, - start_jvm, -) +from scyjava._jvm import jimport, start_jvm +from scyjava._types import JavaClasses, is_jarray, isjava, jarray, jclass, jinstance +from scyjava.config import Mode, mode _logger = logging.getLogger(__name__) @@ -193,11 +184,13 @@ def to_java(obj: Any, **hints: Dict) -> Any: * float values in Double range but outside float range convert to Double * float values outside double range convert to BigDecimal - :param obj: The Python object to convert. - :param hints: An optional dictionary of hints, to help scyjava - make decisions about how to do the conversion. - :returns: A corresponding Java object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. + :param obj: + The Python object to convert. + :param hints: + An optional dictionary of hints, to help scyjava + make decisions about how to do the conversion. + :return: A corresponding Java object with the same contents. + :raise TypeError: if the argument is not one of the aforementioned types. """ start_jvm() return _convert(obj, java_converters, **hints) @@ -206,7 +199,7 @@ def to_java(obj: Any, **hints: Dict) -> Any: def _stock_java_converters() -> List[Converter]: """ Construct the Python-to-Java converters supported out of the box. - :returns: A list of Converters + :return: A list of Converters """ start_jvm() return [ @@ -541,12 +534,15 @@ def to_python(data: Any, gentle: bool = False) -> Any: * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator - :param data: The Java object to convert. - :param gentle: If set, and the type cannot be converted, leaves - the data alone rather than raising a TypeError. - :returns: A corresponding Python object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types, - and the gentle flag is not set. + :param data: + The Java object to convert. + :param gentle: + If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. + :return: A corresponding Python object with the same contents. + :raise TypeError: + if the argument is not one of the aforementioned types, + and the gentle flag is not set. """ start_jvm() try: @@ -560,7 +556,7 @@ def to_python(data: Any, gentle: bool = False) -> Any: def _stock_py_converters() -> List: """ Construct the Java-to-Python converters supported out of the box. - :returns: A list of Converters + :return: A list of Converters """ start_jvm() diff --git a/src/scyjava/_java.py b/src/scyjava/_jvm.py similarity index 59% rename from src/scyjava/_java.py rename to src/scyjava/_jvm.py index e9a7a3c1..2e2350a8 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_jvm.py @@ -1,5 +1,5 @@ """ -Utility functions for working with the Java and JVM. +Utility functions for working with the Java Virtual Machine. """ import atexit @@ -11,7 +11,6 @@ from functools import lru_cache from importlib import import_module from pathlib import Path -from typing import Callable, Sequence import jpype import jpype.config @@ -26,58 +25,6 @@ _shutdown_callbacks = [] -class JavaClasses: - """ - Utility class used to make importing frequently-used Java classes - significantly easier and more readable. - - Benefits: - * Minimal boilerplate - * Lazy evaluation - * Usable within type hints - - Example: - - from scyjava import JavaClasses - - class MyJavaClasses(JavaClasses): - @JavaClasses.java_import - def String(self): return "java.lang.String" - @JavaClasses.java_import - def Integer(self): return "java.lang.Integer" - # ... and many more ... - - jc = MyJavaClasses() - - def parse_number_with_java(s: "jc.String") -> "jc.Integer": - return jc.Integer.parseInt(s) - """ - - def java_import(func: Callable[[], str]) -> Callable[[], jpype.JClass]: - """ - A decorator used to lazily evaluate a java import. - func is a function of a Python class that takes no arguments and - returns a string identifying a Java class by name. - - Using that function, this decorator creates a property - that when called, imports the class identified by the function. - """ - - @property - def inner(self): - if not jvm_started(): - raise RuntimeError("JVM has not started yet!") - try: - return jimport(func(self)) - except TypeError: - return None - - return inner - - -# -- JVM functions -- - - def jvm_version() -> str: """ Gets the version of the JVM as a tuple, with each dot-separated digit @@ -166,8 +113,9 @@ def start_jvm(options=None) -> None: time a scyjava function needing a JVM is invoked, one is started on the fly with the configuration specified via the scijava.config mechanism. - :param options: List of options to pass to the JVM. For example: - ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] + :param options: + List of options to pass to the JVM. + For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] """ # if JVM is already running -- break if jvm_started(): @@ -244,7 +192,7 @@ def start_jvm(options=None) -> None: def shutdown_jvm() -> None: - """Shutdown the JVM. + """Shut down the JVM. This function makes a best effort to clean up Java resources first. In particular, shutdown hooks registered with scyjava.when_jvm_stops @@ -262,7 +210,7 @@ def shutdown_jvm() -> None: Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. - :raises RuntimeError: if this method is called while in Jep mode. + :raise RuntimeError: if this method is called while in Jep mode. """ if not jvm_started(): return @@ -308,9 +256,11 @@ def gc() -> None: This function is a shortcut for Java's System.gc(). - :raise RuntimeError: if the JVM has not yet been started. + :raise RuntimeError: If the JVM has not started yet. """ - _jc.System.gc() + _assert_jvm_started() + System = jimport("java.lang.System") + System.gc() def memory_total() -> int: @@ -329,7 +279,7 @@ def memory_total() -> int: :return: The total memory in bytes. :raise RuntimeError: if the JVM has not yet been started. """ - return int(_jc.Runtime.getRuntime().totalMemory()) + return int(_runtime().totalMemory()) def memory_max() -> int: @@ -347,7 +297,7 @@ def memory_max() -> int: :return: The maximum memory in bytes. :raise RuntimeError: if the JVM has not yet been started. """ - return int(_jc.Runtime.getRuntime().maxMemory()) + return int(_runtime().maxMemory()) def memory_used() -> int: @@ -360,7 +310,7 @@ def memory_used() -> int: :return: The used memory in bytes. :raise RuntimeError: if the JVM has not yet been started. """ - return memory_total() - int(_jc.Runtime.getRuntime().freeMemory()) + return memory_total() - int(_runtime().freeMemory()) def available_processors() -> int: @@ -373,14 +323,14 @@ def available_processors() -> int: :return: The number of available processors. :raise RuntimeError: if the JVM has not yet been started. """ - return int(_jc.Runtime.getRuntime().availableProcessors()) + return int(_runtime().availableProcessors()) def is_jvm_headless() -> bool: """ Return true iff Java is running in headless mode. - :raises RuntimeError: If the JVM has not started yet. + :raise RuntimeError: If the JVM has not started yet. """ if not jvm_started(): raise RuntimeError("JVM has not started yet!") @@ -439,35 +389,15 @@ def when_jvm_stops(f) -> None: _shutdown_callbacks.append(f) -# -- Java functions -- - - -def isjava(data) -> bool: - """Return whether the given data object is a Java object.""" - if mode == Mode.JEP: - return jinstance(data, "java.lang.Object") - - assert mode == Mode.JPYPE - return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) - - -def is_jarray(data) -> bool: - """Return whether the given data object is a Java array.""" - if mode == Mode.JEP: - return str(type(data)) == "" - - assert mode == Mode.JPYPE - return isinstance(data, jpype.JArray) - - @lru_cache(maxsize=None) def jimport(class_name: str): """ Import a class from Java to Python. :param class_name: Name of the class to import. - :returns: A pointer to the class, which can be used to - e.g. instantiate objects of that class. + :return: + A pointer to the class, which can be used to + e.g. instantiate objects of that class. """ if mode == Mode.JEP: module_path = class_name.rsplit(".", 1) @@ -479,185 +409,12 @@ def jimport(class_name: str): return jpype.JClass(class_name) -def jclass(data): - """ - Obtain a Java class object. - - Supported types include: - - A. Name of a class to look up -- e.g. "java.lang.String" -- - which returns the equivalent of Class.forName("java.lang.String"). - - B. A static-style class reference -- e.g. String -- - which returns the equivalent of String.class. - - C. A Java object -- e.g. foo -- - which returns the equivalent of foo.getClass(). - - Note that if you pass a java.lang.Class object, you will get back Class.class, - i.e. the Java class for the Class class. :-) - - :param data: The object from which to glean the class. - :returns: A java.lang.Class object, suitable for use with reflection. - :raises TypeError: if the argument is not one of the aforementioned types. - """ - if isinstance(data, str): - # Name of a class -- case (A) above. - return jclass(jimport(data)) - - if mode == Mode.JPYPE: - start_jvm() - if isinstance(data, jpype.JClass): - # JPype object representing a static-style class -- case (B) above. - return data.class_ - elif mode == Mode.JEP: - if str(type(data.getClass())) == "": - # Jep object representing a static-style class -- case (B) above. - raise ValueError( - "Jep does not support Java class objects " - + "-- see https://github.com/ninia/jep/issues/405" - ) - - # A Java object -- case (C) above. - if jinstance(data, "java.lang.Object"): - return data.getClass() - - raise TypeError("Cannot glean class from data of type: " + str(type(data))) - - -def jinstance(obj, jtype) -> bool: - """ - Test if the given object is an instance of a particular Java type. - - :param obj: The object to check. - :param jtype: The Java type, as either a jimported class or as a string. - :returns: True iff the object is an instance of that Java type. - """ - if isinstance(jtype, str): - jtype = jimport(jtype) - - if mode == Mode.JEP: - return isinstance(obj, jtype.__pytype__) - - assert mode == Mode.JPYPE - return isinstance(obj, jtype) - - -def jstacktrace(exc) -> str: - """ - Extract the Java-side stack trace from a Java exception. - - Example of usage: - - from scyjava import jimport, jstacktrace - try: - Integer = jimport('java.lang.Integer') - nan = Integer.parseInt('not a number') - except Exception as exc: - print(jstacktrace(exc)) - - :param exc: The Java Throwable from which to extract the stack trace. - :returns: A multi-line string containing the stack trace, or empty string - if no stack trace could be extracted. - """ - try: - StringWriter = jimport("java.io.StringWriter") - PrintWriter = jimport("java.io.PrintWriter") - sw = StringWriter() - exc.printStackTrace(PrintWriter(sw, True)) - return str(sw) - except BaseException: - return "" - - -def jarray(kind, lengths: Sequence): - """ - Create a new n-dimensional Java array. - - :param kind: The type of array to create. This can either be a particular - type of object as obtained from jimport, or else a special code for one of - the eight primitive array types: - * 'b' for byte - * 'c' for char - * 'd' for double - * 'f' for float - * 'i' for int - * 'j' for long - * 's' for short - * 'z' for boolean - :param lengths: List of lengths for the array. For example: - `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. - You can pass a single integer to make a 1-dimensional array of that length. - :returns: The newly allocated array - """ - if isinstance(kind, str): - kind = kind.lower() - if isinstance(lengths, int): - lengths = [lengths] - arraytype = kind - - if mode == Mode.JEP: - import jep # noqa: F401 - - if len(lengths) == 1: - # Fast case: 1-d array (we can use primitives) - arr = jep.jarray(lengths[0], arraytype) - else: - # Slow case: n-d array (we cannot use primitives) - # See https://github.com/ninia/jep/issues/439 - kinds = { - "b": jimport("java.lang.Byte"), - "c": jimport("java.lang.Character"), - "d": jimport("java.lang.Double"), - "f": jimport("java.lang.Float"), - "i": jimport("java.lang.Integer"), - "j": jimport("java.lang.Long"), - "s": jimport("java.lang.Short"), - "z": jimport("java.lang.Boolean"), - } - if arraytype in kinds: - arraytype = kinds[arraytype] - kind = arraytype - # build up the array type - for _ in range(len(lengths) - 1): - arraytype = jep.jarray(0, arraytype) - # instantiate the n-dimensional array - arr = jep.jarray(lengths[0], arraytype) - - elif mode == Mode.JPYPE: - start_jvm() - - # build up the array type - kinds = { - "b": jpype.JByte, - "c": jpype.JChar, - "d": jpype.JDouble, - "f": jpype.JFloat, - "i": jpype.JInt, - "j": jpype.JLong, - "s": jpype.JShort, - "z": jpype.JBoolean, - } - if arraytype in kinds: - arraytype = kinds[arraytype] - for _ in range(len(lengths)): - arraytype = jpype.JArray(arraytype) - # instantiate the n-dimensional array - arr = arraytype(lengths[0]) - - if len(lengths) > 1: - for i in range(len(arr)): - arr[i] = jarray(kind, lengths[1:]) - return arr - - -# fmt: off -class _JavaClasses(JavaClasses): - @JavaClasses.java_import - def Runtime(self): return "java.lang.Runtime" # noqa: E272 - @JavaClasses.java_import - def System(self): return "java.lang.System" # noqa: E272 -# fmt: on +def _assert_jvm_started(): + if not jvm_started(): + raise RuntimeError("JVM has not started yet!") -_jc = _JavaClasses() +def _runtime(): + _assert_jvm_started() + Runtime = jimport("java.lang.Runtime") + return Runtime.getRuntime() diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 5a1fd29e..74939f1f 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -13,8 +13,8 @@ from jpype import JImplements, JOverride -from ._convert import to_java -from ._java import jimport +from scyjava._convert import to_java +from scyjava._jvm import jimport def enable_python_scripting(context): diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py new file mode 100644 index 00000000..c064eb7a --- /dev/null +++ b/src/scyjava/_types.py @@ -0,0 +1,334 @@ +""" +Utility functions for working with and reasoning about Java types. +""" +from typing import Any, Callable, Sequence, Tuple, Union + +import jpype + +from scyjava._jvm import jimport, jvm_started, start_jvm +from scyjava.config import Mode, mode + + +class JavaClasses: + """ + Utility class used to make importing frequently-used Java classes + significantly easier and more readable. + + Benefits: + * Minimal boilerplate + * Lazy evaluation + * Usable within type hints + + Example: + + from scyjava import JavaClasses + + class MyJavaClasses(JavaClasses): + @JavaClasses.java_import + def String(self): return "java.lang.String" + @JavaClasses.java_import + def Integer(self): return "java.lang.Integer" + # ... and many more ... + + jc = MyJavaClasses() + + def parse_number_with_java(s: "jc.String") -> "jc.Integer": + return jc.Integer.parseInt(s) + """ + + def java_import(func: Callable[[], str]) -> property: + """ + A decorator used to lazily evaluate a java import. + func is a function of a Python class that takes no arguments and + returns a string identifying a Java class by name. + + Using that function, this decorator creates a property + that when called, imports the class identified by the function. + """ + + @property + def inner(self): + if not jvm_started(): + raise Exception() + try: + return jimport(func(self)) + except TypeError: + return None + + return inner + + +def jclass(data): + """ + Obtain a Java class object. + + Supported types include: + + A. Name of a class to look up -- e.g. "java.lang.String" -- + which returns the equivalent of Class.forName("java.lang.String"). + + B. A static-style class reference -- e.g. String -- + which returns the equivalent of String.class. + + C. A Java object -- e.g. foo -- + which returns the equivalent of foo.getClass(). + + Note that if you pass a java.lang.Class object, you will get back Class.class, + i.e. the Java class for the Class class. :-) + + :param data: The object from which to glean the class. + :return: A java.lang.Class object, suitable for use with reflection. + :raise TypeError: if the argument is not one of the aforementioned types. + """ + if isinstance(data, str): + # Name of a class -- case (A) above. + return jclass(jimport(data)) + + if mode == Mode.JPYPE: + start_jvm() + if isinstance(data, jpype.JClass): + # JPype object representing a static-style class -- case (B) above. + return data.class_ + elif mode == Mode.JEP: + if str(type(data.getClass())) == "": + # Jep object representing a static-style class -- case (B) above. + raise ValueError( + "Jep does not support Java class objects " + + "-- see https://github.com/ninia/jep/issues/405" + ) + + # A Java object -- case (C) above. + if jinstance(data, "java.lang.Object"): + return data.getClass() + + raise TypeError("Cannot glean class from data of type: " + str(type(data))) + + +def jstacktrace(exc) -> str: + """ + Extract the Java-side stack trace from a Java exception. + + Example of usage: + + from scyjava import jimport, jstacktrace + try: + Integer = jimport('java.lang.Integer') + nan = Integer.parseInt('not a number') + except Exception as exc: + print(jstacktrace(exc)) + + :param exc: The Java Throwable from which to extract the stack trace. + :return: A multi-line string containing the stack trace, or empty string + if no stack trace could be extracted. + """ + try: + StringWriter = jimport("java.io.StringWriter") + PrintWriter = jimport("java.io.PrintWriter") + sw = StringWriter() + exc.printStackTrace(PrintWriter(sw, True)) + return str(sw) + except BaseException: + return "" + + +def isjava(data) -> bool: + """Return whether the given data object is a Java object.""" + if mode == Mode.JEP: + return jinstance(data, "java.lang.Object") + + assert mode == Mode.JPYPE + return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) + + +def is_jbyte(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Byte") + + +def is_jshort(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Short") + + +def is_jinteger(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Integer") + + +def is_jlong(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Long") + + +def is_jfloat(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Float") + + +def is_jdouble(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Double") + + +def is_jboolean(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Boolean") + + +def is_jcharacter(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Character") + + +def is_jarray(data: Any) -> bool: + """Return whether the given data object is a Java array.""" + if mode == Mode.JEP: + return str(type(data)) == "" + + assert mode == Mode.JPYPE + return isinstance(data, jpype.JArray) + + +def jinstance(obj, jtype) -> bool: + """ + Test if the given object is an instance of a particular Java type. + + :param obj: The object to check. + :param jtype: The Java type, as either a jimported class or as a string. + :return: True iff the object is an instance of that Java type. + """ + if isinstance(jtype, str): + jtype = jimport(jtype) + + if mode == Mode.JEP: + return isinstance(obj, jtype.__pytype__) + + assert mode == Mode.JPYPE + return isinstance(obj, jtype) + + +def jarray(kind, lengths: Sequence): + """ + Create a new n-dimensional Java array. + + :param kind: The type of array to create. This can either be a particular + type of object as obtained from jimport, or else a special code for one of + the eight primitive array types: + * 'b' for byte + * 'c' for char + * 'd' for double + * 'f' for float + * 'i' for int + * 'j' for long + * 's' for short + * 'z' for boolean + :param lengths: List of lengths for the array. For example: + `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. + You can pass a single integer to make a 1-dimensional array of that length. + :return: The newly allocated array + """ + if isinstance(kind, str): + kind = kind.lower() + if isinstance(lengths, int): + lengths = [lengths] + arraytype = kind + + if mode == Mode.JEP: + import jep # noqa: F401 + + if len(lengths) == 1: + # Fast case: 1-d array (we can use primitives) + arr = jep.jarray(lengths[0], arraytype) + else: + # Slow case: n-d array (we cannot use primitives) + # See https://github.com/ninia/jep/issues/439 + kinds = { + "b": jimport("java.lang.Byte"), + "c": jimport("java.lang.Character"), + "d": jimport("java.lang.Double"), + "f": jimport("java.lang.Float"), + "i": jimport("java.lang.Integer"), + "j": jimport("java.lang.Long"), + "s": jimport("java.lang.Short"), + "z": jimport("java.lang.Boolean"), + } + if arraytype in kinds: + arraytype = kinds[arraytype] + kind = arraytype + # build up the array type + for _ in range(len(lengths) - 1): + arraytype = jep.jarray(0, arraytype) + # instantiate the n-dimensional array + arr = jep.jarray(lengths[0], arraytype) + + elif mode == Mode.JPYPE: + start_jvm() + + # build up the array type + kinds = { + "b": jpype.JByte, + "c": jpype.JChar, + "d": jpype.JDouble, + "f": jpype.JFloat, + "i": jpype.JInt, + "j": jpype.JLong, + "s": jpype.JShort, + "z": jpype.JBoolean, + } + if arraytype in kinds: + arraytype = kinds[arraytype] + for _ in range(len(lengths)): + arraytype = jpype.JArray(arraytype) + # instantiate the n-dimensional array + arr = arraytype(lengths[0]) + + if len(lengths) > 1: + for i in range(len(arr)): + arr[i] = jarray(kind, lengths[1:]) + return arr + + +def numeric_bounds( + the_type: type, +) -> Union[Tuple[int, int], Tuple[float, float], Tuple[None, None]]: + """ + Get the minimum and maximum values for the given numeric type. + For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), + whereas a Java double returns (float(-Double.MAX_VALUE), float(Double.MAX_VALUE)). + + :param the_type: The type whose minimum and maximum values are needed. + :return: + The minimum and maximum values as a two-element tuple of int or float, + or a two-element tuple of None if no known bounds. + """ + if is_jbyte(the_type): + Byte = jimport("java.lang.Byte") + return int(Byte.MIN_VALUE), int(Byte.MAX_VALUE) + + if is_jshort(the_type): + Short = jimport("java.lang.Short") + return int(Short.MIN_VALUE), int(Short.MAX_VALUE) + + if is_jinteger(the_type): + Integer = jimport("java.lang.Integer") + return int(Integer.MIN_VALUE), int(Integer.MAX_VALUE) + + if is_jlong(the_type): + Long = jimport("java.lang.Long") + return int(Long.MIN_VALUE), int(Long.MAX_VALUE) + + if is_jfloat(the_type): + Float = jimport("java.lang.Float") + return float(-Float.MAX_VALUE), float(Float.MAX_VALUE) + + if is_jdouble(the_type): + Double = jimport("java.lang.Double") + return float(-Double.MAX_VALUE), float(Double.MAX_VALUE) + + return None, None + + +def _is_jtype(the_type: type, class_name: str) -> bool: + """ + Test if the given type object is *exactly* the specified Java type. + + :param the_type: The type object to check. + :param class_name: The fully qualified Java class name in string form. + :return: True iff the type is exactly that Java type. + """ + # NB: Stringify the type to support both bridge modes. Ex: + # * JPype: + # * Jep: + return f"class '{class_name}'" in str(the_type) diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index 532c590f..c1695db7 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -5,7 +5,8 @@ import logging from importlib.metadata import version -from ._java import isjava, jimport +from scyjava._jvm import jimport +from scyjava._types import isjava _logger = logging.getLogger(__name__) diff --git a/tests/test_basics.py b/tests/test_basics.py index b36eb402..c858b672 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -8,12 +8,12 @@ class TestBasics(object): """ - Tests basic scyjava functions. + Test basic scyjava functions. """ def test_jclass(self): """ - Tests the jclass function. + Test the jclass function. """ if mode == Mode.JEP: pytest.skip("Jep does not support Java class objects!") @@ -23,7 +23,7 @@ def test_jclass(self): def test_jimport(self): """ - Tests the jimport function. + Test the jimport function. """ Object = scyjava.jimport("java.lang.Object") assert Object is not None @@ -34,7 +34,7 @@ def test_jimport(self): def test_jinstance(self): """ - Tests the jinstance function. + Test the jinstance function. """ jstr = scyjava.to_java("Hello") assert scyjava.jinstance(jstr, "java.lang.String") diff --git a/tests/test_convert.py b/tests/test_convert.py index a02db1e8..e9f0489d 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -27,7 +27,7 @@ class TestConvert(object): def testClass(self): """ - Tests class detection from Java objects. + Test class detection from Java objects. """ if mode == Mode.JEP: pytest.skip("The jclass function does not work yet in Jep mode.") diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 00000000..cc6adc44 --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,32 @@ +from scyjava import numeric_bounds, to_java + + +class TestTypes(object): + """ + Test Java-type-related functions. + """ + + def test_numeric_bounds(self): + v_byte = to_java(1, type="byte") + v_short = to_java(2, type="short") + v_int = to_java(3, type="int") + v_long = to_java(4, type="long") + v_bigint = to_java(5, type="bigint") + v_float = to_java(6.7, type="float") + v_double = to_java(7.8, type="double") + v_bigdec = to_java(8.9, type="bigdec") + + assert (-128, 127) == numeric_bounds(type(v_byte)) + assert (-32768, 32767) == numeric_bounds(type(v_short)) + assert (-2147483648, 2147483647) == numeric_bounds(type(v_int)) + assert (-9223372036854775808, 9223372036854775807) == numeric_bounds( + type(v_long) + ) + assert (None, None) == numeric_bounds(type(v_bigint)) + assert (-3.4028234663852886e38, 3.4028234663852886e38) == numeric_bounds( + type(v_float) + ) + assert (-1.7976931348623157e308, 1.7976931348623157e308) == numeric_bounds( + type(v_double) + ) + assert (None, None) == numeric_bounds(type(v_bigdec))