From 0a8141cd2c25c30e5a3a1aa023a4576e5ee76d93 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Mar 2025 13:27:11 -0500 Subject: [PATCH 01/41] Add some functions for Java object introspection --- src/scyjava/__init__.py | 1 + src/scyjava/_types.py | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index f622685..4ba3a41 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,6 +124,7 @@ jclass, jinstance, jstacktrace, + methods, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index ef0318a..fa0ae97 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -324,6 +324,55 @@ def numeric_bounds( return None, None +def methods(data) -> list[dict[str, Any]]: + """ + Use Java reflection to introspect the given Java object, + returning a table of its available methods. + + :param data: The object or class to inspect. + :return: List of table rows with columns "name", "arguments", and "returns". + """ + + if not isjava(data): + raise ValueError("Not a Java object") + + cls = data if jinstance(data, "java.lang.Class") else jclass(data) + + methods = cls.getMethods() + + # NB: Methods are returned in inconsistent order. + # Arrays.sort(methods, (m1, m2) -> { + # final int nameComp = m1.getName().compareTo(m2.getName()) + # if (nameComp != 0) return nameComp + # final int pCount1 = m1.getParameterCount() + # final int pCount2 = m2.getParameterCount() + # if (pCount1 != pCount2) return pCount1 - pCount2 + # final Class[] pTypes1 = m1.getParameterTypes() + # final Class[] pTypes2 = m2.getParameterTypes() + # for (int i = 0; i < pTypes1.length; i++) { + # final int typeComp = ClassUtils.compare(pTypes1[i], pTypes2[i]) + # if (typeComp != 0) return typeComp + # } + # return ClassUtils.compare(m1.getReturnType(), m2.getReturnType()) + # }) + + table = [] + + for m in methods: + name = m.getName() + args = [c.getName() for c in m.getParameterTypes()] + returns = m.getReturnType().getName() + table.append( + { + "name": name, + "arguments": args, + "returns": returns, + } + ) + + return table + + def _is_jtype(the_type: type, class_name: str) -> bool: """ Test if the given type object is *exactly* the specified Java type. From 7b8a6c4b08f6786bc7123948716516cc039b6646 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Wed, 26 Mar 2025 22:15:19 -0700 Subject: [PATCH 02/41] Update methods() functionality --- src/scyjava/__init__.py | 1 + src/scyjava/_types.py | 55 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 4ba3a41..139e591 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -125,6 +125,7 @@ jinstance, jstacktrace, methods, + find_java_methods, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index fa0ae97..f69c6fa 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -324,7 +324,7 @@ def numeric_bounds( return None, None -def methods(data) -> list[dict[str, Any]]: +def find_java_methods(data) -> list[dict[str, Any]]: """ Use Java reflection to introspect the given Java object, returning a table of its available methods. @@ -369,8 +369,59 @@ def methods(data) -> list[dict[str, Any]]: "returns": returns, } ) + sorted_table = sorted(table, key=lambda d: d["name"]) - return table + return sorted_table + + +def map_syntax(base_type): + """ + Maps a java BaseType annotation (see link below) in an Java array + to a specific type with an Python interpretable syntax. + https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 + """ + basetype_mapping = { + "[B": "byte[]", + "[C": "char[]", + "[D": "double[]", + "[F": "float[]", + "[C": "int[]", + "[J": "long[]", + "[L": "[]", # array + "[S": "short[]", + "[Z": "boolean[]", + } + + if base_type in basetype_mapping: + return basetype_mapping[base_type] + elif base_type.__str__().startswith("[L"): + return base_type.__str__()[2:-1] + "[]" + else: + return base_type + + +def methods(data) -> str: + table = find_java_methods(data) + + offset = max(list(map(lambda l: len(l["returns"]), table))) + all_methods = "" + + for entry in table: + entry["returns"] = map_syntax(entry["returns"]) + entry["arguments"] = [map_syntax(e) for e in entry["arguments"]] + + if not entry["arguments"]: + all_methods = ( + all_methods + + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}()\n' + ) + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + all_methods = ( + all_methods + + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}({arg_string})\n' + ) + print(all_methods) def _is_jtype(the_type: type, class_name: str) -> bool: From fe0bfe391d24a7b4b37f2b38e2ebb138c8c9d41b Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 00:29:02 -0700 Subject: [PATCH 03/41] Make progress on introspection methods --- src/scyjava/_types.py | 105 +++++++++++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index f69c6fa..fa53eae 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -330,7 +330,7 @@ def find_java_methods(data) -> list[dict[str, Any]]: returning a table of its available methods. :param data: The object or class to inspect. - :return: List of table rows with columns "name", "arguments", and "returns". + :return: List of table rows with columns "name", "static", "arguments", and "returns". """ if not isjava(data): @@ -357,14 +357,17 @@ def find_java_methods(data) -> list[dict[str, Any]]: # }) table = [] + Modifier = jimport("java.lang.reflect.Modifier") for m in methods: name = m.getName() args = [c.getName() for c in m.getParameterTypes()] + mods = Modifier.isStatic(m.getModifiers()) returns = m.getReturnType().getName() table.append( { "name": name, + "static": mods, "arguments": args, "returns": returns, } @@ -374,7 +377,31 @@ def find_java_methods(data) -> list[dict[str, Any]]: return sorted_table -def map_syntax(base_type): +# TODO +def find_java_fields(data) -> list[dict[str, Any]]: + """ + Use Java reflection to introspect the given Java object, + returning a table of its available fields. + + :param data: The object or class to inspect. + :return: List of table rows with columns "name", "arguments", and "returns". + """ + if not isjava(data): + raise ValueError("Not a Java object") + + cls = data if jinstance(data, "java.lang.Class") else jclass(data) + + fields = cls.getFields() + table = [] + + for f in fields: + name = f.getName() + table.append(name) + + return table + + +def _map_syntax(base_type): """ Maps a java BaseType annotation (see link below) in an Java array to a specific type with an Python interpretable syntax. @@ -385,7 +412,7 @@ def map_syntax(base_type): "[C": "char[]", "[D": "double[]", "[F": "float[]", - "[C": "int[]", + "[I": "int[]", "[J": "long[]", "[L": "[]", # array "[S": "short[]", @@ -394,33 +421,77 @@ def map_syntax(base_type): if base_type in basetype_mapping: return basetype_mapping[base_type] + # Handle the case of a returned array of an object elif base_type.__str__().startswith("[L"): return base_type.__str__()[2:-1] + "[]" else: return base_type +def _make_pretty_string(entry, offset): + """ + Prints the entry with a specific formatting and aligned style + :param entry: Dictionary of class names, modifiers, arguments, and return values. + :param offset: Offset between the return value and the method. + """ + + # A star implies that the method is a static method + return_val = f'{entry["returns"].__str__():<{offset}}' + # Handle whether to print static/instance modifiers + obj_name = f'{entry["name"]}' + modifier = f'{"*":>4}' if entry["static"] else f'{"":>4}' + + # Handle methods with no arguments + if not entry["arguments"]: + return f"{return_val} {modifier} = {obj_name}()\n" + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + return f"{return_val} {modifier} = {obj_name}({arg_string})\n" + + +# TODO +def fields(data) -> str: + """ + Writes data to a printed field names with the field value. + :param data: The object or class to inspect. + """ + table = find_java_fields(data) + + all_fields = "" + ################ + # FILL THIS IN # + ################ + + print(all_fields) + + +# TODO +def attrs(data): + """ + Writes data to a printed field names with the field value. Alias for `fields(data)`. + :param data: The object or class to inspect. + """ + fields(data) + + def methods(data) -> str: + """ + Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. + + :param data: The object or class to inspect. + """ table = find_java_methods(data) offset = max(list(map(lambda l: len(l["returns"]), table))) all_methods = "" - for entry in table: - entry["returns"] = map_syntax(entry["returns"]) - entry["arguments"] = [map_syntax(e) for e in entry["arguments"]] + entry["returns"] = _map_syntax(entry["returns"]) + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string - if not entry["arguments"]: - all_methods = ( - all_methods - + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}()\n' - ) - else: - arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) - all_methods = ( - all_methods - + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}({arg_string})\n' - ) + # 4 added to align the asterisk with output. + print(f'{"":<{offset+4}}* indicates a static method') print(all_methods) From 25d769e11c0c394121b3f520d853432ab09fe017 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 00:34:05 -0700 Subject: [PATCH 04/41] Make linter happy --- src/scyjava/_types.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index fa53eae..c797ae0 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -436,10 +436,10 @@ def _make_pretty_string(entry, offset): """ # A star implies that the method is a static method - return_val = f'{entry["returns"].__str__():<{offset}}' + return_val = f"{entry['returns'].__str__():<{offset}}" # Handle whether to print static/instance modifiers - obj_name = f'{entry["name"]}' - modifier = f'{"*":>4}' if entry["static"] else f'{"":>4}' + obj_name = f"{entry['name']}" + modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" # Handle methods with no arguments if not entry["arguments"]: @@ -455,7 +455,7 @@ def fields(data) -> str: Writes data to a printed field names with the field value. :param data: The object or class to inspect. """ - table = find_java_fields(data) + # table = find_java_fields(data) all_fields = "" ################ @@ -482,7 +482,7 @@ def methods(data) -> str: """ table = find_java_methods(data) - offset = max(list(map(lambda l: len(l["returns"]), table))) + offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" for entry in table: entry["returns"] = _map_syntax(entry["returns"]) @@ -491,7 +491,7 @@ def methods(data) -> str: all_methods += entry_string # 4 added to align the asterisk with output. - print(f'{"":<{offset+4}}* indicates a static method') + print(f"{'':<{offset + 4}}* indicates a static method") print(all_methods) From 1a0fd7123c965f67f3b91c6b8783cf8786bf0f70 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 11:49:54 -0700 Subject: [PATCH 05/41] Add source code reporting to methods() function --- src/scyjava/_types.py | 57 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index c797ae0..479e4e5 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -336,9 +336,9 @@ def find_java_methods(data) -> list[dict[str, Any]]: if not isjava(data): raise ValueError("Not a Java object") - cls = data if jinstance(data, "java.lang.Class") else jclass(data) + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - methods = cls.getMethods() + methods = jcls.getMethods() # NB: Methods are returned in inconsistent order. # Arrays.sort(methods, (m1, m2) -> { @@ -389,9 +389,9 @@ def find_java_fields(data) -> list[dict[str, Any]]: if not isjava(data): raise ValueError("Not a Java object") - cls = data if jinstance(data, "java.lang.Class") else jclass(data) + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - fields = cls.getFields() + fields = jcls.getFields() table = [] for f in fields: @@ -474,21 +474,64 @@ def attrs(data): fields(data) -def methods(data) -> str: +def get_source_code(data): + """ + Tries to find the source code using Scijava's SourceFinder' + :param data: The object or class to check for source code. + """ + types = jimport("org.scijava.util.Types") + sf = jimport("org.scijava.search.SourceFinder") + jstring = jimport("java.lang.String") + try: + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) + if types.location(jcls).toString().startsWith(jstring("jrt")): + # Handles Java RunTime (jrt) exceptions. + return "GitHub source code not available" + url = sf.sourceLocation(jcls, None) + urlstring = url.toString() + return urlstring + except jimport("java.lang.IllegalArgumentException") as err: + return f"Illegal argument provided {err=}, {type(err)=}" + except Exception as err: + return f"Unexpected {err=}, {type(err)=}" + + +def methods(data, static: bool | None = None, source: bool = True) -> str: """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. :param data: The object or class to inspect. + :param static: Which methods to print. Can be set as boolean to filter the class methods based on + static vs. instance methods. Optional, default is None (prints all methods). + :param source: Whether to print any available source code. Default True. """ table = find_java_methods(data) + # Print source code offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" + if source: + urlstring = get_source_code(data) + print(f"URL: {urlstring}") + else: + pass + + # Print methods for entry in table: entry["returns"] = _map_syntax(entry["returns"]) entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string + if static is None: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + + elif static and entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + elif not static and not entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + else: + continue # 4 added to align the asterisk with output. print(f"{'':<{offset + 4}}* indicates a static method") From 640d57deb7393407fe6edcced2d728186133e3bb Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 12:47:48 -0700 Subject: [PATCH 06/41] Implement fields introspection function --- src/scyjava/__init__.py | 5 ++- src/scyjava/_types.py | 67 ++++++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 139e591..be38678 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,8 +124,11 @@ jclass, jinstance, jstacktrace, - methods, find_java_methods, + find_java_fields, + methods, + fields, + attrs, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 479e4e5..24e9387 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -396,9 +396,11 @@ def find_java_fields(data) -> list[dict[str, Any]]: for f in fields: name = f.getName() - table.append(name) + ftype = f.getType().getName() + table.append({"name": name, "type": ftype}) + sorted_table = sorted(table, key=lambda d: d["name"]) - return table + return sorted_table def _map_syntax(base_type): @@ -441,39 +443,18 @@ def _make_pretty_string(entry, offset): obj_name = f"{entry['name']}" modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" + # Handle fields + if entry["arguments"] is None: + return f"{return_val} = {obj_name}\n" + # Handle methods with no arguments - if not entry["arguments"]: + if len(entry["arguments"]) == 0: return f"{return_val} {modifier} = {obj_name}()\n" else: arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -# TODO -def fields(data) -> str: - """ - Writes data to a printed field names with the field value. - :param data: The object or class to inspect. - """ - # table = find_java_fields(data) - - all_fields = "" - ################ - # FILL THIS IN # - ################ - - print(all_fields) - - -# TODO -def attrs(data): - """ - Writes data to a printed field names with the field value. Alias for `fields(data)`. - :param data: The object or class to inspect. - """ - fields(data) - - def get_source_code(data): """ Tries to find the source code using Scijava's SourceFinder' @@ -496,6 +477,36 @@ def get_source_code(data): return f"Unexpected {err=}, {type(err)=}" +def fields(data) -> str: + """ + Writes data to a printed field names with the field value. + :param data: The object or class to inspect. + """ + table = find_java_fields(data) + if len(table) == 0: + print("No fields found") + return + + all_fields = "" + offset = max(list(map(lambda entry: len(entry["type"]), table))) + for entry in table: + entry["returns"] = _map_syntax(entry["type"]) + entry["static"] = False + entry["arguments"] = None + entry_string = _make_pretty_string(entry, offset) + all_fields += entry_string + + print(all_fields) + + +def attrs(data): + """ + Writes data to a printed field names with the field value. Alias for `fields(data)`. + :param data: The object or class to inspect. + """ + fields(data) + + def methods(data, static: bool | None = None, source: bool = True) -> str: """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. From aa3996c22b2d3e76932969a314725a92847a128d Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 13:49:01 -0700 Subject: [PATCH 07/41] Add partials, refactor, add java_source function --- src/scyjava/__init__.py | 6 +- src/scyjava/_types.py | 162 ++++++++++++++++------------------------ 2 files changed, 70 insertions(+), 98 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index be38678..043374f 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,11 +124,13 @@ jclass, jinstance, jstacktrace, - find_java_methods, - find_java_fields, + find_java, + java_source, methods, fields, attrs, + src, + java_source, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 24e9387..6fdced3 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -5,6 +5,7 @@ from typing import Any, Callable, Sequence, Tuple, Union import jpype +from functools import partial from scyjava._jvm import jimport, jvm_started, start_jvm from scyjava.config import Mode, mode @@ -324,46 +325,43 @@ def numeric_bounds( return None, None -def find_java_methods(data) -> list[dict[str, Any]]: +def find_java(data, aspect: str) -> list[dict[str, Any]]: """ Use Java reflection to introspect the given Java object, returning a table of its available methods. - :param data: The object or class to inspect. - :return: List of table rows with columns "name", "static", "arguments", and "returns". + :param data: The object or class or fully qualified class name to inspect. + :param aspect: Either 'methods' or 'fields' + :return: List of dicts with keys: "name", "static", "arguments", and "returns". """ - if not isjava(data): - raise ValueError("Not a Java object") + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) + except: + raise ValueError("Not a Java object") + Modifier = jimport("java.lang.reflect.Modifier") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - methods = jcls.getMethods() - - # NB: Methods are returned in inconsistent order. - # Arrays.sort(methods, (m1, m2) -> { - # final int nameComp = m1.getName().compareTo(m2.getName()) - # if (nameComp != 0) return nameComp - # final int pCount1 = m1.getParameterCount() - # final int pCount2 = m2.getParameterCount() - # if (pCount1 != pCount2) return pCount1 - pCount2 - # final Class[] pTypes1 = m1.getParameterTypes() - # final Class[] pTypes2 = m2.getParameterTypes() - # for (int i = 0; i < pTypes1.length; i++) { - # final int typeComp = ClassUtils.compare(pTypes1[i], pTypes2[i]) - # if (typeComp != 0) return typeComp - # } - # return ClassUtils.compare(m1.getReturnType(), m2.getReturnType()) - # }) + if aspect == "methods": + cls_aspects = jcls.getMethods() + elif aspect == "fields": + cls_aspects = jcls.getFields() + else: + return "`aspect` must be either 'fields' or 'methods'" table = [] - Modifier = jimport("java.lang.reflect.Modifier") - for m in methods: + for m in cls_aspects: name = m.getName() - args = [c.getName() for c in m.getParameterTypes()] + if aspect == "methods": + args = [c.getName() for c in m.getParameterTypes()] + returns = m.getReturnType().getName() + elif aspect == "fields": + args = None + returns = m.getType().getName() mods = Modifier.isStatic(m.getModifiers()) - returns = m.getReturnType().getName() table.append( { "name": name, @@ -377,32 +375,6 @@ def find_java_methods(data) -> list[dict[str, Any]]: return sorted_table -# TODO -def find_java_fields(data) -> list[dict[str, Any]]: - """ - Use Java reflection to introspect the given Java object, - returning a table of its available fields. - - :param data: The object or class to inspect. - :return: List of table rows with columns "name", "arguments", and "returns". - """ - if not isjava(data): - raise ValueError("Not a Java object") - - jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - - fields = jcls.getFields() - table = [] - - for f in fields: - name = f.getName() - ftype = f.getType().getName() - table.append({"name": name, "type": ftype}) - sorted_table = sorted(table, key=lambda d: d["name"]) - - return sorted_table - - def _map_syntax(base_type): """ Maps a java BaseType annotation (see link below) in an Java array @@ -445,7 +417,7 @@ def _make_pretty_string(entry, offset): # Handle fields if entry["arguments"] is None: - return f"{return_val} = {obj_name}\n" + return f"{return_val} {modifier} = {obj_name}\n" # Handle methods with no arguments if len(entry["arguments"]) == 0: @@ -455,82 +427,65 @@ def _make_pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def get_source_code(data): +def java_source(data): """ Tries to find the source code using Scijava's SourceFinder' - :param data: The object or class to check for source code. + :param data: The object or class or fully qualified class name to check for source code. + :return: The URL of the java class """ types = jimport("org.scijava.util.Types") sf = jimport("org.scijava.search.SourceFinder") jstring = jimport("java.lang.String") try: + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) # check if data can be imported + except: + raise ValueError("Not a Java object") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) if types.location(jcls).toString().startsWith(jstring("jrt")): # Handles Java RunTime (jrt) exceptions. - return "GitHub source code not available" + raise ValueError("Java Builtin: GitHub source code not available") url = sf.sourceLocation(jcls, None) urlstring = url.toString() return urlstring except jimport("java.lang.IllegalArgumentException") as err: return f"Illegal argument provided {err=}, {type(err)=}" + except ValueError as err: + return f"{err}" + except TypeError: + return f"Not a Java class {str(type(data))}" except Exception as err: return f"Unexpected {err=}, {type(err)=}" -def fields(data) -> str: - """ - Writes data to a printed field names with the field value. - :param data: The object or class to inspect. - """ - table = find_java_fields(data) - if len(table) == 0: - print("No fields found") - return - - all_fields = "" - offset = max(list(map(lambda entry: len(entry["type"]), table))) - for entry in table: - entry["returns"] = _map_syntax(entry["type"]) - entry["static"] = False - entry["arguments"] = None - entry_string = _make_pretty_string(entry, offset) - all_fields += entry_string - - print(all_fields) - - -def attrs(data): - """ - Writes data to a printed field names with the field value. Alias for `fields(data)`. - :param data: The object or class to inspect. - """ - fields(data) - - -def methods(data, static: bool | None = None, source: bool = True) -> str: +def _print_data(data, aspect, static: bool | None = None, source: bool = True): """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. :param data: The object or class to inspect. - :param static: Which methods to print. Can be set as boolean to filter the class methods based on - static vs. instance methods. Optional, default is None (prints all methods). + :param aspect: Whether to print class fields or methods. + :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on + static vs. instance methods. Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ - table = find_java_methods(data) + table = find_java(data, aspect) + if len(table) == 0: + print(f"No {aspect} found") + return # Print source code offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" if source: - urlstring = get_source_code(data) - print(f"URL: {urlstring}") - else: - pass + urlstring = java_source(data) + print(f"Source code URL: {urlstring}") # Print methods for entry in table: entry["returns"] = _map_syntax(entry["returns"]) - entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + if entry["arguments"]: + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] if static is None: entry_string = _make_pretty_string(entry, offset) all_methods += entry_string @@ -545,10 +500,25 @@ def methods(data, static: bool | None = None, source: bool = True) -> str: continue # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates a static method") + print(f"{'':<{offset + 4}}* indicates static modifier") print(all_methods) +methods = partial(_print_data, aspect="methods") +fields = partial(_print_data, aspect="fields") +attrs = partial(_print_data, aspect="fields") + + +def src(data): + """ + Prints the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string + """ + source_url = java_source(data) + print(f"Source code URL: {source_url}") + + def _is_jtype(the_type: type, class_name: str) -> bool: """ Test if the given type object is *exactly* the specified Java type. From 9352ff6a98620882e503d4ccb60b55ac210f4d6c Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 13:55:53 -0700 Subject: [PATCH 08/41] Refactor introspection code --- src/scyjava/__init__.py | 4 +- src/scyjava/_introspection.py | 203 ++++++++++++++++++++++++++++++++++ src/scyjava/_types.py | 195 -------------------------------- 3 files changed, 206 insertions(+), 196 deletions(-) create mode 100644 src/scyjava/_introspection.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 043374f..b30f909 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,6 +124,9 @@ jclass, jinstance, jstacktrace, + numeric_bounds, +) +from ._introspection import ( find_java, java_source, methods, @@ -131,7 +134,6 @@ attrs, src, java_source, - numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspection.py new file mode 100644 index 0000000..f136eaa --- /dev/null +++ b/src/scyjava/_introspection.py @@ -0,0 +1,203 @@ +""" +Introspection functions for reporting java classes and URL +""" + +from functools import partial +from typing import Any + +from scyjava._jvm import jimport +from scyjava._types import isjava, jinstance, jclass + + +def find_java(data, aspect: str) -> list[dict[str, Any]]: + """ + Use Java reflection to introspect the given Java object, + returning a table of its available methods. + + :param data: The object or class or fully qualified class name to inspect. + :param aspect: Either 'methods' or 'fields' + :return: List of dicts with keys: "name", "static", "arguments", and "returns". + """ + + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) + except: + raise ValueError("Not a Java object") + + Modifier = jimport("java.lang.reflect.Modifier") + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) + + if aspect == "methods": + cls_aspects = jcls.getMethods() + elif aspect == "fields": + cls_aspects = jcls.getFields() + else: + return "`aspect` must be either 'fields' or 'methods'" + + table = [] + + for m in cls_aspects: + name = m.getName() + if aspect == "methods": + args = [c.getName() for c in m.getParameterTypes()] + returns = m.getReturnType().getName() + elif aspect == "fields": + args = None + returns = m.getType().getName() + mods = Modifier.isStatic(m.getModifiers()) + table.append( + { + "name": name, + "static": mods, + "arguments": args, + "returns": returns, + } + ) + sorted_table = sorted(table, key=lambda d: d["name"]) + + return sorted_table + + +def _map_syntax(base_type): + """ + Maps a java BaseType annotation (see link below) in an Java array + to a specific type with an Python interpretable syntax. + https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 + """ + basetype_mapping = { + "[B": "byte[]", + "[C": "char[]", + "[D": "double[]", + "[F": "float[]", + "[I": "int[]", + "[J": "long[]", + "[L": "[]", # array + "[S": "short[]", + "[Z": "boolean[]", + } + + if base_type in basetype_mapping: + return basetype_mapping[base_type] + # Handle the case of a returned array of an object + elif base_type.__str__().startswith("[L"): + return base_type.__str__()[2:-1] + "[]" + else: + return base_type + + +def _make_pretty_string(entry, offset): + """ + Prints the entry with a specific formatting and aligned style + :param entry: Dictionary of class names, modifiers, arguments, and return values. + :param offset: Offset between the return value and the method. + """ + + # A star implies that the method is a static method + return_val = f"{entry['returns'].__str__():<{offset}}" + # Handle whether to print static/instance modifiers + obj_name = f"{entry['name']}" + modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" + + # Handle fields + if entry["arguments"] is None: + return f"{return_val} {modifier} = {obj_name}\n" + + # Handle methods with no arguments + if len(entry["arguments"]) == 0: + return f"{return_val} {modifier} = {obj_name}()\n" + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + return f"{return_val} {modifier} = {obj_name}({arg_string})\n" + + +def java_source(data): + """ + Tries to find the source code using Scijava's SourceFinder' + :param data: The object or class or fully qualified class name to check for source code. + :return: The URL of the java class + """ + types = jimport("org.scijava.util.Types") + sf = jimport("org.scijava.search.SourceFinder") + jstring = jimport("java.lang.String") + try: + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) # check if data can be imported + except: + raise ValueError("Not a Java object") + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) + if types.location(jcls).toString().startsWith(jstring("jrt")): + # Handles Java RunTime (jrt) exceptions. + raise ValueError("Java Builtin: GitHub source code not available") + url = sf.sourceLocation(jcls, None) + urlstring = url.toString() + return urlstring + except jimport("java.lang.IllegalArgumentException") as err: + return f"Illegal argument provided {err=}, {type(err)=}" + except ValueError as err: + return f"{err}" + except TypeError: + return f"Not a Java class {str(type(data))}" + except Exception as err: + return f"Unexpected {err=}, {type(err)=}" + + +def _print_data(data, aspect, static: bool | None = None, source: bool = True): + """ + Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. + + :param data: The object or class to inspect. + :param aspect: Whether to print class fields or methods. + :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on + static vs. instance methods. Optional, default is None (prints all). + :param source: Whether to print any available source code. Default True. + """ + table = find_java(data, aspect) + if len(table) == 0: + print(f"No {aspect} found") + return + + # Print source code + offset = max(list(map(lambda entry: len(entry["returns"]), table))) + all_methods = "" + if source: + urlstring = java_source(data) + print(f"Source code URL: {urlstring}") + + # Print methods + for entry in table: + entry["returns"] = _map_syntax(entry["returns"]) + if entry["arguments"]: + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + if static is None: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + + elif static and entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + elif not static and not entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + else: + continue + + # 4 added to align the asterisk with output. + print(f"{'':<{offset + 4}}* indicates static modifier") + print(all_methods) + + +methods = partial(_print_data, aspect="methods") +fields = partial(_print_data, aspect="fields") +attrs = partial(_print_data, aspect="fields") + + +def src(data): + """ + Prints the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string + """ + source_url = java_source(data) + print(f"Source code URL: {source_url}") diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 6fdced3..ef0318a 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -5,7 +5,6 @@ from typing import Any, Callable, Sequence, Tuple, Union import jpype -from functools import partial from scyjava._jvm import jimport, jvm_started, start_jvm from scyjava.config import Mode, mode @@ -325,200 +324,6 @@ def numeric_bounds( return None, None -def find_java(data, aspect: str) -> list[dict[str, Any]]: - """ - Use Java reflection to introspect the given Java object, - returning a table of its available methods. - - :param data: The object or class or fully qualified class name to inspect. - :param aspect: Either 'methods' or 'fields' - :return: List of dicts with keys: "name", "static", "arguments", and "returns". - """ - - if not isjava(data) and isinstance(data, str): - try: - data = jimport(data) - except: - raise ValueError("Not a Java object") - - Modifier = jimport("java.lang.reflect.Modifier") - jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - - if aspect == "methods": - cls_aspects = jcls.getMethods() - elif aspect == "fields": - cls_aspects = jcls.getFields() - else: - return "`aspect` must be either 'fields' or 'methods'" - - table = [] - - for m in cls_aspects: - name = m.getName() - if aspect == "methods": - args = [c.getName() for c in m.getParameterTypes()] - returns = m.getReturnType().getName() - elif aspect == "fields": - args = None - returns = m.getType().getName() - mods = Modifier.isStatic(m.getModifiers()) - table.append( - { - "name": name, - "static": mods, - "arguments": args, - "returns": returns, - } - ) - sorted_table = sorted(table, key=lambda d: d["name"]) - - return sorted_table - - -def _map_syntax(base_type): - """ - Maps a java BaseType annotation (see link below) in an Java array - to a specific type with an Python interpretable syntax. - https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 - """ - basetype_mapping = { - "[B": "byte[]", - "[C": "char[]", - "[D": "double[]", - "[F": "float[]", - "[I": "int[]", - "[J": "long[]", - "[L": "[]", # array - "[S": "short[]", - "[Z": "boolean[]", - } - - if base_type in basetype_mapping: - return basetype_mapping[base_type] - # Handle the case of a returned array of an object - elif base_type.__str__().startswith("[L"): - return base_type.__str__()[2:-1] + "[]" - else: - return base_type - - -def _make_pretty_string(entry, offset): - """ - Prints the entry with a specific formatting and aligned style - :param entry: Dictionary of class names, modifiers, arguments, and return values. - :param offset: Offset between the return value and the method. - """ - - # A star implies that the method is a static method - return_val = f"{entry['returns'].__str__():<{offset}}" - # Handle whether to print static/instance modifiers - obj_name = f"{entry['name']}" - modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" - - # Handle fields - if entry["arguments"] is None: - return f"{return_val} {modifier} = {obj_name}\n" - - # Handle methods with no arguments - if len(entry["arguments"]) == 0: - return f"{return_val} {modifier} = {obj_name}()\n" - else: - arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) - return f"{return_val} {modifier} = {obj_name}({arg_string})\n" - - -def java_source(data): - """ - Tries to find the source code using Scijava's SourceFinder' - :param data: The object or class or fully qualified class name to check for source code. - :return: The URL of the java class - """ - types = jimport("org.scijava.util.Types") - sf = jimport("org.scijava.search.SourceFinder") - jstring = jimport("java.lang.String") - try: - if not isjava(data) and isinstance(data, str): - try: - data = jimport(data) # check if data can be imported - except: - raise ValueError("Not a Java object") - jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - if types.location(jcls).toString().startsWith(jstring("jrt")): - # Handles Java RunTime (jrt) exceptions. - raise ValueError("Java Builtin: GitHub source code not available") - url = sf.sourceLocation(jcls, None) - urlstring = url.toString() - return urlstring - except jimport("java.lang.IllegalArgumentException") as err: - return f"Illegal argument provided {err=}, {type(err)=}" - except ValueError as err: - return f"{err}" - except TypeError: - return f"Not a Java class {str(type(data))}" - except Exception as err: - return f"Unexpected {err=}, {type(err)=}" - - -def _print_data(data, aspect, static: bool | None = None, source: bool = True): - """ - Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. - - :param data: The object or class to inspect. - :param aspect: Whether to print class fields or methods. - :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on - static vs. instance methods. Optional, default is None (prints all). - :param source: Whether to print any available source code. Default True. - """ - table = find_java(data, aspect) - if len(table) == 0: - print(f"No {aspect} found") - return - - # Print source code - offset = max(list(map(lambda entry: len(entry["returns"]), table))) - all_methods = "" - if source: - urlstring = java_source(data) - print(f"Source code URL: {urlstring}") - - # Print methods - for entry in table: - entry["returns"] = _map_syntax(entry["returns"]) - if entry["arguments"]: - entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] - if static is None: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - - elif static and entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - elif not static and not entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - else: - continue - - # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates static modifier") - print(all_methods) - - -methods = partial(_print_data, aspect="methods") -fields = partial(_print_data, aspect="fields") -attrs = partial(_print_data, aspect="fields") - - -def src(data): - """ - Prints the source code URL for a Java class, object, or class name. - - :param data: The Java class, object, or fully qualified class name as string - """ - source_url = java_source(data) - print(f"Source code URL: {source_url}") - - def _is_jtype(the_type: type, class_name: str) -> bool: """ Test if the given type object is *exactly* the specified Java type. From fe602176b9505bffaf1db0ba4eb9ebf0d989b0f7 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 15:16:13 -0700 Subject: [PATCH 09/41] Add test cases for introspection functions --- tests/test_introspection.py | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_introspection.py diff --git a/tests/test_introspection.py b/tests/test_introspection.py new file mode 100644 index 0000000..e8405f3 --- /dev/null +++ b/tests/test_introspection.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 28 13:58:54 2025 + +@author: ian +""" + +import scyjava +from scyjava.config import Mode, mode + +scyjava.config.endpoints.append("net.imagej:imagej") +scyjava.config.endpoints.append("net.imagej:imagej-legacy:MANAGED") + + +class TestIntrospection(object): + """ + Test introspection functionality. + """ + + def test_find_java_methods(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_String = "java.lang.String" + String = scyjava.jimport(str_String) + str_Obj = scyjava.find_java(str_String, "methods") + jimport_Obj = scyjava.find_java(String, "methods") + assert len(str_Obj) > 0 + assert len(jimport_Obj) > 0 + assert jimport_Obj is not None + assert jimport_Obj == str_Obj + + def test_find_java_fields(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_BitSet = "java.util.BitSet" + BitSet = scyjava.jimport(str_BitSet) + str_Obj = scyjava.find_java(str_BitSet, "fields") + bitset_Obj = scyjava.find_java(BitSet, "fields") + assert len(str_Obj) == 0 + assert len(bitset_Obj) == 0 + assert bitset_Obj is not None + assert bitset_Obj == str_Obj + + def test_find_source(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_SF = "org.scijava.search.SourceFinder" + SF = scyjava.jimport(str_SF) + source_strSF = scyjava.java_source(str_SF) + source_SF = scyjava.java_source(SF) + github_home = "https://github.com/" + assert source_strSF.startsWith(github_home) + assert source_SF.startsWith(github_home) + assert source_strSF == source_SF + + def test_imagej_legacy(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_RE = "ij.plugin.RoiEnlarger" + table = scyjava.find_java(str_RE, aspect="methods") + assert len([entry for entry in table if entry["static"]]) == 3 + github_home = "https://github.com/" + assert scyjava.java_source(str_RE).startsWith(github_home) From 7293d2343f4d8c6575f5461489719012127df0b8 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 15:26:45 -0700 Subject: [PATCH 10/41] Lint code --- src/scyjava/__init__.py | 1 - src/scyjava/_introspection.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index b30f909..c922cd0 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -133,7 +133,6 @@ fields, attrs, src, - java_source, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspection.py index f136eaa..9cbf199 100644 --- a/src/scyjava/_introspection.py +++ b/src/scyjava/_introspection.py @@ -22,8 +22,8 @@ def find_java(data, aspect: str) -> list[dict[str, Any]]: if not isjava(data) and isinstance(data, str): try: data = jimport(data) - except: - raise ValueError("Not a Java object") + except Exception as err: + raise ValueError(f"Not a Java object {err}") Modifier = jimport("java.lang.reflect.Modifier") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) @@ -124,8 +124,8 @@ def java_source(data): if not isjava(data) and isinstance(data, str): try: data = jimport(data) # check if data can be imported - except: - raise ValueError("Not a Java object") + except Exception as err: + raise ValueError(f"Not a Java object {err}") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) if types.location(jcls).toString().startsWith(jstring("jrt")): # Handles Java RunTime (jrt) exceptions. From 41230787cc12512a7bcbe8e17f42207437312fdc Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Sun, 30 Mar 2025 23:37:27 -0700 Subject: [PATCH 11/41] Improve introspection function documentation --- src/scyjava/_introspection.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspection.py index 9cbf199..18e097e 100644 --- a/src/scyjava/_introspection.py +++ b/src/scyjava/_introspection.py @@ -1,5 +1,5 @@ """ -Introspection functions for reporting java classes and URL +Introspection functions for reporting java class 'methods', 'fields', and source code URL. """ from functools import partial @@ -61,7 +61,7 @@ def find_java(data, aspect: str) -> list[dict[str, Any]]: def _map_syntax(base_type): """ - Maps a java BaseType annotation (see link below) in an Java array + Maps a Java BaseType annotation (see link below) in an Java array to a specific type with an Python interpretable syntax. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 """ @@ -113,7 +113,7 @@ def _make_pretty_string(entry, offset): def java_source(data): """ - Tries to find the source code using Scijava's SourceFinder' + Tries to find the source code using Scijava's SourceFinder :param data: The object or class or fully qualified class name to check for source code. :return: The URL of the java class """ @@ -147,10 +147,9 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. - :param data: The object or class to inspect. - :param aspect: Whether to print class fields or methods. - :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on - static vs. instance methods. Optional, default is None (prints all). + :param data: The object or class to inspect or fully qualified class name. + :param aspect: Whether to print class 'fields' or 'methods'. + :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ table = find_java(data, aspect) @@ -188,6 +187,7 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): print(all_methods) +# The functions with short names for quick usage. methods = partial(_print_data, aspect="methods") fields = partial(_print_data, aspect="fields") attrs = partial(_print_data, aspect="fields") From 3533446337f8c43e57df729872b4ef6387c19d65 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Sun, 30 Mar 2025 23:40:37 -0700 Subject: [PATCH 12/41] Add docstring to test_introspection.py --- tests/test_introspection.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_introspection.py b/tests/test_introspection.py index e8405f3..98cdaf4 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ -Created on Fri Mar 28 13:58:54 2025 +Tests for introspection of java classes (fields and methods), as well as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 -@author: ian +@author: ian-coccimiglio """ import scyjava From 9516a72eaed28c6fd52424a682738a56b00c5e9a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:25:12 -0500 Subject: [PATCH 13/41] Wrap long line --- tests/test_introspection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_introspection.py b/tests/test_introspection.py index 98cdaf4..37504ec 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -1,5 +1,6 @@ """ -Tests for introspection of java classes (fields and methods), as well as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 +Tests for introspection of java classes (fields and methods), as well +as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 @author: ian-coccimiglio """ From 652022322727a62ffcf38a5c2771e11b469df631 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:26:16 -0500 Subject: [PATCH 14/41] Increment minor version digit The introspection functions are new API. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bbcc8b8..3a559b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.10.3.dev0" +version = "1.11.0.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 363e7bc6cd459282a88a037c5e368131b53aba3c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:29:35 -0500 Subject: [PATCH 15/41] Alphabetize introspection imports --- src/scyjava/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index c922cd0..555effd 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -91,6 +91,14 @@ to_java, to_python, ) +from ._introspection import ( + attrs, + fields, + find_java, + java_source, + methods, + src, +) from ._jvm import ( # noqa: F401 available_processors, gc, @@ -126,14 +134,6 @@ jstacktrace, numeric_bounds, ) -from ._introspection import ( - find_java, - java_source, - methods, - fields, - attrs, - src, -) from ._versions import compare_version, get_version, is_version_at_least __version__ = get_version("scyjava") From c8afba44be7595c909da35d2934829c7c2e2ae44 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:30:20 -0500 Subject: [PATCH 16/41] Shorten introspection to introspect For conversion functions, we use the name `convert`. So let's be consistent with introspection functions. It's also more concise. --- src/scyjava/__init__.py | 2 +- src/scyjava/{_introspection.py => _introspect.py} | 0 tests/{test_introspection.py => test_introspect.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/scyjava/{_introspection.py => _introspect.py} (100%) rename tests/{test_introspection.py => test_introspect.py} (100%) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 555effd..e7a8e61 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -91,7 +91,7 @@ to_java, to_python, ) -from ._introspection import ( +from ._introspect import ( attrs, fields, find_java, diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspect.py similarity index 100% rename from src/scyjava/_introspection.py rename to src/scyjava/_introspect.py diff --git a/tests/test_introspection.py b/tests/test_introspect.py similarity index 100% rename from tests/test_introspection.py rename to tests/test_introspect.py From 6bfec82e7eab7e70f6fe6a73c900717a48445aa5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:43:02 -0500 Subject: [PATCH 17/41] Add toplevel docstrings to test files --- tests/test_arrays.py | 4 ++++ tests/test_basics.py | 4 ++++ tests/test_convert.py | 4 ++++ tests/test_introspect.py | 4 ++-- tests/test_pandas.py | 4 ++++ tests/test_types.py | 4 ++++ tests/test_version.py | 4 ++++ 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_arrays.py b/tests/test_arrays.py index fca796d..80f1891 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -1,3 +1,7 @@ +""" +Tests for array-related functions in _types submodule. +""" + import numpy as np from scyjava import is_jarray, jarray, to_python diff --git a/tests/test_basics.py b/tests/test_basics.py index 042b436..76e2229 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,3 +1,7 @@ +""" +Tests for key functions across all scyjava submodules. +""" + import re import pytest diff --git a/tests/test_convert.py b/tests/test_convert.py index e9f0489..fcfabe1 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _convert submodule. +""" + import math from os import getcwd from pathlib import Path diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 37504ec..a0d5872 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -1,6 +1,6 @@ """ -Tests for introspection of java classes (fields and methods), as well -as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 +Tests for functions in _introspect submodule. +Created on Fri Mar 28 13:58:54 2025 @author: ian-coccimiglio """ diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 8fc4bc3..c18d243 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _pandas submodule. +""" + import numpy as np import numpy.testing as npt import pandas as pd diff --git a/tests/test_types.py b/tests/test_types.py index 7ccf23b..e4bdbc9 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _types submodule. +""" + from scyjava import jclass, jimport, numeric_bounds, to_java from scyjava.config import Mode, mode diff --git a/tests/test_version.py b/tests/test_version.py index 5411387..3b5fafc 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _versions submodule. +""" + from pathlib import Path import toml From 37bd92e2b25f2ba87268767971b6c130bee90d48 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:45:13 -0500 Subject: [PATCH 18/41] Fix naming of versions test file The submodule is called versions; let's name the test file consistently. --- tests/{test_version.py => test_versions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_version.py => test_versions.py} (100%) diff --git a/tests/test_version.py b/tests/test_versions.py similarity index 100% rename from tests/test_version.py rename to tests/test_versions.py From 5f883f1119f9b0e05a371b0dbe69faa848ba0aab Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 13:45:19 -0500 Subject: [PATCH 19/41] Fix type hints to work with Python 3.8 --- src/scyjava/_introspect.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 18e097e..50d54bd 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -3,13 +3,13 @@ """ from functools import partial -from typing import Any +from typing import Any, Dict, List, Optional from scyjava._jvm import jimport from scyjava._types import isjava, jinstance, jclass -def find_java(data, aspect: str) -> list[dict[str, Any]]: +def find_java(data, aspect: str) -> List[Dict[str, Any]]: """ Use Java reflection to introspect the given Java object, returning a table of its available methods. @@ -143,7 +143,7 @@ def java_source(data): return f"Unexpected {err=}, {type(err)=}" -def _print_data(data, aspect, static: bool | None = None, source: bool = True): +def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. From 6282d8c399ed6381f6c2f85d09c8daaad40e1b08 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 13:45:31 -0500 Subject: [PATCH 20/41] CI: test Python 3.13 support --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b246a0..09a2f3d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: python-version: [ '3.8', '3.10', - '3.12' + '3.13' ] steps: From bded14f5e267eca69f71c376b11aed0b419980d7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:11:32 -0500 Subject: [PATCH 21/41] Rename find_java function to jreflect To me, the name `find_java` suggests we will be locating a JVM installation, rather than "finding" information about Java objects. The information doesn't need to be "found" or "located", but rather only introspected or interrogated. Technically, I suppose "introspection" implies read/access while "reflection" implies write/mutation, but `jintrospect` is rather clunky, whereas the term "reflection" is widely known in both Java and Python circles. --- src/scyjava/__init__.py | 2 +- src/scyjava/_introspect.py | 6 +++--- tests/test_introspect.py | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index e7a8e61..e42b51a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -94,8 +94,8 @@ from ._introspect import ( attrs, fields, - find_java, java_source, + jreflect, methods, src, ) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 50d54bd..643969a 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -9,10 +9,10 @@ from scyjava._types import isjava, jinstance, jclass -def find_java(data, aspect: str) -> List[Dict[str, Any]]: +def jreflect(data, aspect: str) -> List[Dict[str, Any]]: """ Use Java reflection to introspect the given Java object, - returning a table of its available methods. + returning a table of its available methods or fields. :param data: The object or class or fully qualified class name to inspect. :param aspect: Either 'methods' or 'fields' @@ -152,7 +152,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ - table = find_java(data, aspect) + table = jreflect(data, aspect) if len(table) == 0: print(f"No {aspect} found") return diff --git a/tests/test_introspect.py b/tests/test_introspect.py index a0d5872..a12c051 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -17,27 +17,27 @@ class TestIntrospection(object): Test introspection functionality. """ - def test_find_java_methods(self): + def test_jreflect_methods(self): if mode == Mode.JEP: # JEP does not support the jclass function. return str_String = "java.lang.String" String = scyjava.jimport(str_String) - str_Obj = scyjava.find_java(str_String, "methods") - jimport_Obj = scyjava.find_java(String, "methods") + str_Obj = scyjava.jreflect(str_String, "methods") + jimport_Obj = scyjava.jreflect(String, "methods") assert len(str_Obj) > 0 assert len(jimport_Obj) > 0 assert jimport_Obj is not None assert jimport_Obj == str_Obj - def test_find_java_fields(self): + def test_jreflect_fields(self): if mode == Mode.JEP: # JEP does not support the jclass function. return str_BitSet = "java.util.BitSet" BitSet = scyjava.jimport(str_BitSet) - str_Obj = scyjava.find_java(str_BitSet, "fields") - bitset_Obj = scyjava.find_java(BitSet, "fields") + str_Obj = scyjava.jreflect(str_BitSet, "fields") + bitset_Obj = scyjava.jreflect(BitSet, "fields") assert len(str_Obj) == 0 assert len(bitset_Obj) == 0 assert bitset_Obj is not None @@ -61,7 +61,7 @@ def test_imagej_legacy(self): # JEP does not support the jclass function. return str_RE = "ij.plugin.RoiEnlarger" - table = scyjava.find_java(str_RE, aspect="methods") + table = scyjava.jreflect(str_RE, aspect="methods") assert len([entry for entry in table if entry["static"]]) == 3 github_home = "https://github.com/" assert scyjava.java_source(str_RE).startsWith(github_home) From 0778a893f11809643d3f6f1f6785bb79d761386a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:37:54 -0500 Subject: [PATCH 22/41] Add missing is_j* type methods to README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index eb22bbb..5a2cff1 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,22 @@ FUNCTIONS is_jarray(data: Any) -> bool Return whether the given data object is a Java array. + is_jboolean(the_type: type) -> bool + + is_jbyte(the_type: type) -> bool + + is_jcharacter(the_type: type) -> bool + + is_jdouble(the_type: type) -> bool + + is_jfloat(the_type: type) -> bool + + is_jinteger(the_type: type) -> bool + + is_jlong(the_type: type) -> bool + + is_jshort(the_type: type) -> bool + is_jvm_headless() -> bool Return true iff Java is running in headless mode. From 21ffae9015b182e38c7ffb228cbf62b8db64b4cf Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:45:40 -0500 Subject: [PATCH 23/41] Use imperative tense for function docstrings --- src/scyjava/_introspect.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 643969a..bc5aa4c 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -61,7 +61,7 @@ def jreflect(data, aspect: str) -> List[Dict[str, Any]]: def _map_syntax(base_type): """ - Maps a Java BaseType annotation (see link below) in an Java array + Map a Java BaseType annotation (see link below) in an Java array to a specific type with an Python interpretable syntax. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 """ @@ -88,7 +88,7 @@ def _map_syntax(base_type): def _make_pretty_string(entry, offset): """ - Prints the entry with a specific formatting and aligned style + Print the entry with a specific formatting and aligned style. :param entry: Dictionary of class names, modifiers, arguments, and return values. :param offset: Offset between the return value and the method. """ @@ -113,7 +113,7 @@ def _make_pretty_string(entry, offset): def java_source(data): """ - Tries to find the source code using Scijava's SourceFinder + Try to find the source code using SciJava's SourceFinder. :param data: The object or class or fully qualified class name to check for source code. :return: The URL of the java class """ @@ -145,7 +145,8 @@ def java_source(data): def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): """ - Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. + Write data to a printed string of class methods with inputs, static modifier, + arguments, and return values. :param data: The object or class to inspect or fully qualified class name. :param aspect: Whether to print class 'fields' or 'methods'. @@ -195,7 +196,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True def src(data): """ - Prints the source code URL for a Java class, object, or class name. + Print the source code URL for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string """ From bb06ed1f1999542fb3eba0dde63fd47232aeedfc Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:46:14 -0500 Subject: [PATCH 24/41] Wrap >88 lines, and make quoting more consistent --- src/scyjava/_introspect.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index bc5aa4c..17b844f 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -1,5 +1,6 @@ """ -Introspection functions for reporting java class 'methods', 'fields', and source code URL. +Introspection functions for reporting Java +class methods, fields, and source code URL. """ from functools import partial @@ -15,7 +16,7 @@ def jreflect(data, aspect: str) -> List[Dict[str, Any]]: returning a table of its available methods or fields. :param data: The object or class or fully qualified class name to inspect. - :param aspect: Either 'methods' or 'fields' + :param aspect: Either "methods" or "fields" :return: List of dicts with keys: "name", "static", "arguments", and "returns". """ @@ -33,7 +34,7 @@ def jreflect(data, aspect: str) -> List[Dict[str, Any]]: elif aspect == "fields": cls_aspects = jcls.getFields() else: - return "`aspect` must be either 'fields' or 'methods'" + return '`aspect` must be either "fields" or "methods"' table = [] @@ -114,7 +115,8 @@ def _make_pretty_string(entry, offset): def java_source(data): """ Try to find the source code using SciJava's SourceFinder. - :param data: The object or class or fully qualified class name to check for source code. + :param data: + The object or class or fully qualified class name to check for source code. :return: The URL of the java class """ types = jimport("org.scijava.util.Types") @@ -149,8 +151,10 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True arguments, and return values. :param data: The object or class to inspect or fully qualified class name. - :param aspect: Whether to print class 'fields' or 'methods'. - :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). + :param aspect: Whether to print class "fields" or "methods". + :param static: + Boolean filter on Static or Instance methods. + Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ table = jreflect(data, aspect) From 553d552a4316ac0229fa3ea1f01cf014f54ad477 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:46:49 -0500 Subject: [PATCH 25/41] Add introspection functions to the README --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 5a2cff1..8891917 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,12 @@ FUNCTIONS You can pass a single integer to make a 1-dimensional array of that length. :return: The newly allocated array + java_source(data) + Try to find the source code using SciJava's SourceFinder. + :param data: + The object or class or fully qualified class name to check for source code. + :return: The URL of the java class + jclass(data) Obtain a Java class object. @@ -319,6 +325,14 @@ FUNCTIONS :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. + jreflect(data, aspect: str) -> List[Dict[str, Any]] + Use Java reflection to introspect the given Java object, + returning a table of its available methods or fields. + + :param data: The object or class or fully qualified class name to inspect. + :param aspect: Either "methods" or "fields" + :return: List of dicts with keys: "name", "static", "arguments", and "returns". + jstacktrace(exc) -> str Extract the Java-side stack trace from a Java exception. @@ -427,6 +441,11 @@ FUNCTIONS :raise RuntimeError: if this method is called while in Jep mode. + src(data) + Print the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string + start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling From bf2ee8235dbfbd93c63e0135c3a75d19b83c0ea1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:09:05 -0500 Subject: [PATCH 26/41] Improve get_version method --- src/scyjava/_versions.py | 16 ++++++++++++---- tests/test_versions.py | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index c1695db..f163219 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -15,8 +15,8 @@ def get_version(java_class_or_python_package) -> str: """ Return the version of a Java class or Python package. - For Python package, uses importlib.metadata.version if available - (Python 3.8+), with pkg_resources.get_distribution as a fallback. + For Python packages, invokes importlib.metadata.version on the given + object's base __module__ or __package__ (before the first dot symbol). For Java classes, requires org.scijava:scijava-common on the classpath. @@ -32,8 +32,16 @@ def get_version(java_class_or_python_package) -> str: VersionUtils = jimport("org.scijava.util.VersionUtils") return str(VersionUtils.getVersion(java_class_or_python_package)) - # Assume we were given a Python package name. - return version(java_class_or_python_package) + # Assume we were given a Python package name or module. + package_name = None + if hasattr(java_class_or_python_package, "__module__"): + package_name = java_class_or_python_package.__module__ + elif hasattr(java_class_or_python_package, "__package__"): + package_name = java_class_or_python_package.__package__ + else: + package_name = str(java_class_or_python_package) + + return version(package_name.split(".")[0]) def is_version_at_least(actual_version: str, minimum_version: str) -> bool: diff --git a/tests/test_versions.py b/tests/test_versions.py index 3b5fafc..d588a0b 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -2,6 +2,7 @@ Tests for functions in _versions submodule. """ +from importlib.metadata import version from pathlib import Path import toml @@ -18,8 +19,18 @@ def _expected_version(): def test_version(): - # First, ensure that the version is correct - assert _expected_version() == scyjava.__version__ + sjver = _expected_version() - # Then, ensure that we get the correct version via get_version - assert _expected_version() == scyjava.get_version("scyjava") + # First, ensure that the version is correct. + assert sjver == scyjava.__version__ + + # Then, ensure that we get the correct version via get_version. + assert sjver == scyjava.get_version("scyjava") + assert sjver == scyjava.get_version(scyjava) + assert sjver == scyjava.get_version("scyjava.config") + assert sjver == scyjava.get_version(scyjava.config) + assert sjver == scyjava.get_version(scyjava.config.mode) + assert sjver == scyjava.get_version(scyjava.config.Mode) + + # And that we get the correct version of other things, too. + assert version("toml") == scyjava.get_version(toml) From 2d44fed2690032bf86191c8daa4218c8da92ea3b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:10:55 -0500 Subject: [PATCH 27/41] Test a little further into the GitHub source paths --- tests/test_introspect.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_introspect.py b/tests/test_introspect.py index a12c051..f4bb485 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -51,9 +51,9 @@ def test_find_source(self): SF = scyjava.jimport(str_SF) source_strSF = scyjava.java_source(str_SF) source_SF = scyjava.java_source(SF) - github_home = "https://github.com/" - assert source_strSF.startsWith(github_home) - assert source_SF.startsWith(github_home) + repo_path = "https://github.com/scijava/scijava-search/" + assert source_strSF.startsWith(repo_path) + assert source_SF.startsWith(repo_path) assert source_strSF == source_SF def test_imagej_legacy(self): @@ -63,5 +63,5 @@ def test_imagej_legacy(self): str_RE = "ij.plugin.RoiEnlarger" table = scyjava.jreflect(str_RE, aspect="methods") assert len([entry for entry in table if entry["static"]]) == 3 - github_home = "https://github.com/" - assert scyjava.java_source(str_RE).startsWith(github_home) + repo_path = "https://github.com/imagej/ImageJ/" + assert scyjava.java_source(str_RE).startsWith(repo_path) From 8846ce5429474e98e5941ff4425ca21553962631 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:15:42 -0500 Subject: [PATCH 28/41] Tweak management of multiple endpoints For consistency with scripting integration tests. --- tests/test_introspect.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_introspect.py b/tests/test_introspect.py index f4bb485..3b7d659 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -8,8 +8,9 @@ import scyjava from scyjava.config import Mode, mode -scyjava.config.endpoints.append("net.imagej:imagej") -scyjava.config.endpoints.append("net.imagej:imagej-legacy:MANAGED") +scyjava.config.endpoints.extend( + ["net.imagej:imagej", "net.imagej:imagej-legacy:MANAGED"] +) class TestIntrospection(object): From a31701b331ae09bfbc2518591411dcd3b2cee869 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:27:05 -0500 Subject: [PATCH 29/41] Rename java_source method to jsource More consistent with the rest of the library. --- README.md | 2 +- src/scyjava/__init__.py | 2 +- src/scyjava/_introspect.py | 6 +++--- tests/test_introspect.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8891917..e78a7c6 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ FUNCTIONS You can pass a single integer to make a 1-dimensional array of that length. :return: The newly allocated array - java_source(data) + jsource(data) Try to find the source code using SciJava's SourceFinder. :param data: The object or class or fully qualified class name to check for source code. diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index e42b51a..166099a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -94,8 +94,8 @@ from ._introspect import ( attrs, fields, - java_source, jreflect, + jsource, methods, src, ) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 17b844f..dc5bb67 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -112,7 +112,7 @@ def _make_pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def java_source(data): +def jsource(data): """ Try to find the source code using SciJava's SourceFinder. :param data: @@ -166,7 +166,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" if source: - urlstring = java_source(data) + urlstring = jsource(data) print(f"Source code URL: {urlstring}") # Print methods @@ -204,5 +204,5 @@ def src(data): :param data: The Java class, object, or fully qualified class name as string """ - source_url = java_source(data) + source_url = jsource(data) print(f"Source code URL: {source_url}") diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 3b7d659..3108c20 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -44,14 +44,14 @@ def test_jreflect_fields(self): assert bitset_Obj is not None assert bitset_Obj == str_Obj - def test_find_source(self): + def test_jsource(self): if mode == Mode.JEP: # JEP does not support the jclass function. return str_SF = "org.scijava.search.SourceFinder" SF = scyjava.jimport(str_SF) - source_strSF = scyjava.java_source(str_SF) - source_SF = scyjava.java_source(SF) + source_strSF = scyjava.jsource(str_SF) + source_SF = scyjava.jsource(SF) repo_path = "https://github.com/scijava/scijava-search/" assert source_strSF.startsWith(repo_path) assert source_SF.startsWith(repo_path) @@ -65,4 +65,4 @@ def test_imagej_legacy(self): table = scyjava.jreflect(str_RE, aspect="methods") assert len([entry for entry in table if entry["static"]]) == 3 repo_path = "https://github.com/imagej/ImageJ/" - assert scyjava.java_source(str_RE).startsWith(repo_path) + assert scyjava.jsource(str_RE).startsWith(repo_path) From 2713b6cc44ddf5ba0c9a261033a04f331ea26c02 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 17:18:02 -0500 Subject: [PATCH 30/41] Make jreflect function more powerful --- README.md | 8 ++--- src/scyjava/_introspect.py | 69 ++++++++++++++++++++++++-------------- tests/test_introspect.py | 2 +- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e78a7c6..81337fc 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ AttributeError: 'list' object has no attribute 'stream' Traceback (most recent call last): File "", line 1, in TypeError: No matching overloads found for java.util.Set.addAll(set), options are: - public abstract boolean java.util.Set.addAll(java.util.Collection) + public abstract boolean java.util.Set.addAll(java.util.Collection) >>> from scyjava import to_java as p2j >>> jset.addAll(p2j(pset)) True @@ -325,13 +325,13 @@ FUNCTIONS :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. - jreflect(data, aspect: str) -> List[Dict[str, Any]] + jreflect(data, aspect: str = "all") -> List[Dict[str, Any]] Use Java reflection to introspect the given Java object, returning a table of its available methods or fields. :param data: The object or class or fully qualified class name to inspect. - :param aspect: Either "methods" or "fields" - :return: List of dicts with keys: "name", "static", "arguments", and "returns". + :param aspect: One of: "all", "constructors", "fields", or "methods". + :return: List of dicts with keys: "name", "mods", "arguments", and "returns". jstacktrace(exc) -> str Extract the Java-side stack trace from a Java exception. diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index dc5bb67..413aa0d 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -10,54 +10,73 @@ class methods, fields, and source code URL. from scyjava._types import isjava, jinstance, jclass -def jreflect(data, aspect: str) -> List[Dict[str, Any]]: +def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: """ Use Java reflection to introspect the given Java object, returning a table of its available methods or fields. :param data: The object or class or fully qualified class name to inspect. - :param aspect: Either "methods" or "fields" - :return: List of dicts with keys: "name", "static", "arguments", and "returns". + :param aspect: One of: "all", "constructors", "fields", or "methods". + :return: List of dicts with keys: "name", "mods", "arguments", and "returns". """ + aspects = ["all", "constructors", "fields", "methods"] + if aspect not in aspects: + raise ValueError("aspect must be one of {aspects}") + if not isjava(data) and isinstance(data, str): try: data = jimport(data) - except Exception as err: - raise ValueError(f"Not a Java object {err}") + except Exception as e: + raise ValueError( + f"Object of type '{type(data).__name__}' is not a Java object" + ) from e - Modifier = jimport("java.lang.reflect.Modifier") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - if aspect == "methods": - cls_aspects = jcls.getMethods() - elif aspect == "fields": - cls_aspects = jcls.getFields() - else: - return '`aspect` must be either "fields" or "methods"' + Modifier = jimport("java.lang.reflect.Modifier") + modifiers = { + attr[2:].lower(): getattr(Modifier, attr) + for attr in dir(Modifier) + if attr.startswith("is") + } + + members = [] + if aspect in ["all", "constructors"]: + members.extend(jcls.getConstructors()) + if aspect in ["all", "fields"]: + members.extend(jcls.getFields()) + if aspect in ["all", "methods"]: + members.extend(jcls.getMethods()) table = [] - for m in cls_aspects: - name = m.getName() - if aspect == "methods": - args = [c.getName() for c in m.getParameterTypes()] - returns = m.getReturnType().getName() - elif aspect == "fields": - args = None - returns = m.getType().getName() - mods = Modifier.isStatic(m.getModifiers()) + for member in members: + mtype = str(member.getClass().getName()).split(".")[-1].lower() + name = member.getName() + modflags = member.getModifiers() + mods = [name for name, hasmod in modifiers.items() if hasmod(modflags)] + args = ( + [ptype.getName() for ptype in member.getParameterTypes()] + if hasattr(member, "getParameterTypes") + else None + ) + returns = ( + member.getReturnType().getName() + if hasattr(member, "getReturnType") + else (member.getType().getName() if hasattr(member, "getType") else None) + ) table.append( { + "type": mtype, "name": name, - "static": mods, + "mods": mods, "arguments": args, "returns": returns, } ) - sorted_table = sorted(table, key=lambda d: d["name"]) - return sorted_table + return table def _map_syntax(base_type): @@ -98,7 +117,7 @@ def _make_pretty_string(entry, offset): return_val = f"{entry['returns'].__str__():<{offset}}" # Handle whether to print static/instance modifiers obj_name = f"{entry['name']}" - modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" + modifier = f"{'*':>4}" if "static" in entry["mods"] else f"{'':>4}" # Handle fields if entry["arguments"] is None: diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 3108c20..09eb692 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -63,6 +63,6 @@ def test_imagej_legacy(self): return str_RE = "ij.plugin.RoiEnlarger" table = scyjava.jreflect(str_RE, aspect="methods") - assert len([entry for entry in table if entry["static"]]) == 3 + assert sum(1 for entry in table if "static" in entry["mods"]) == 3 repo_path = "https://github.com/imagej/ImageJ/" assert scyjava.jsource(str_RE).startsWith(repo_path) From 92d7fb154e89df78a6f6c81d5f018680efaa54be Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 19:59:01 -0500 Subject: [PATCH 31/41] Split pretty-print functions to own subpackage * scyjava.fields -> scyjava.inspect.fields * scyjava.methods -> scyjava.inspect.methods * scyjava.src -> scyjava.inspect.src And add new `constructors` and `members` convenience functions. --- README.md | 5 -- src/scyjava/__init__.py | 4 - src/scyjava/_introspect.py | 120 +--------------------------- src/scyjava/inspect.py | 155 +++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 127 deletions(-) create mode 100644 src/scyjava/inspect.py diff --git a/README.md b/README.md index 81337fc..cedcfcb 100644 --- a/README.md +++ b/README.md @@ -441,11 +441,6 @@ FUNCTIONS :raise RuntimeError: if this method is called while in Jep mode. - src(data) - Print the source code URL for a Java class, object, or class name. - - :param data: The Java class, object, or fully qualified class name as string - start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 166099a..4f7c6a2 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -92,12 +92,8 @@ to_python, ) from ._introspect import ( - attrs, - fields, jreflect, jsource, - methods, - src, ) from ._jvm import ( # noqa: F401 available_processors, diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 413aa0d..067abe7 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -3,8 +3,7 @@ class methods, fields, and source code URL. """ -from functools import partial -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from scyjava._jvm import jimport from scyjava._types import isjava, jinstance, jclass @@ -64,7 +63,7 @@ def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: returns = ( member.getReturnType().getName() if hasattr(member, "getReturnType") - else (member.getType().getName() if hasattr(member, "getType") else None) + else (member.getType().getName() if hasattr(member, "getType") else name) ) table.append( { @@ -79,58 +78,6 @@ def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: return table -def _map_syntax(base_type): - """ - Map a Java BaseType annotation (see link below) in an Java array - to a specific type with an Python interpretable syntax. - https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 - """ - basetype_mapping = { - "[B": "byte[]", - "[C": "char[]", - "[D": "double[]", - "[F": "float[]", - "[I": "int[]", - "[J": "long[]", - "[L": "[]", # array - "[S": "short[]", - "[Z": "boolean[]", - } - - if base_type in basetype_mapping: - return basetype_mapping[base_type] - # Handle the case of a returned array of an object - elif base_type.__str__().startswith("[L"): - return base_type.__str__()[2:-1] + "[]" - else: - return base_type - - -def _make_pretty_string(entry, offset): - """ - Print the entry with a specific formatting and aligned style. - :param entry: Dictionary of class names, modifiers, arguments, and return values. - :param offset: Offset between the return value and the method. - """ - - # A star implies that the method is a static method - return_val = f"{entry['returns'].__str__():<{offset}}" - # Handle whether to print static/instance modifiers - obj_name = f"{entry['name']}" - modifier = f"{'*':>4}" if "static" in entry["mods"] else f"{'':>4}" - - # Handle fields - if entry["arguments"] is None: - return f"{return_val} {modifier} = {obj_name}\n" - - # Handle methods with no arguments - if len(entry["arguments"]) == 0: - return f"{return_val} {modifier} = {obj_name}()\n" - else: - arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) - return f"{return_val} {modifier} = {obj_name}({arg_string})\n" - - def jsource(data): """ Try to find the source code using SciJava's SourceFinder. @@ -162,66 +109,3 @@ def jsource(data): return f"Not a Java class {str(type(data))}" except Exception as err: return f"Unexpected {err=}, {type(err)=}" - - -def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): - """ - Write data to a printed string of class methods with inputs, static modifier, - arguments, and return values. - - :param data: The object or class to inspect or fully qualified class name. - :param aspect: Whether to print class "fields" or "methods". - :param static: - Boolean filter on Static or Instance methods. - Optional, default is None (prints all). - :param source: Whether to print any available source code. Default True. - """ - table = jreflect(data, aspect) - if len(table) == 0: - print(f"No {aspect} found") - return - - # Print source code - offset = max(list(map(lambda entry: len(entry["returns"]), table))) - all_methods = "" - if source: - urlstring = jsource(data) - print(f"Source code URL: {urlstring}") - - # Print methods - for entry in table: - entry["returns"] = _map_syntax(entry["returns"]) - if entry["arguments"]: - entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] - if static is None: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - - elif static and entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - elif not static and not entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - else: - continue - - # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates static modifier") - print(all_methods) - - -# The functions with short names for quick usage. -methods = partial(_print_data, aspect="methods") -fields = partial(_print_data, aspect="fields") -attrs = partial(_print_data, aspect="fields") - - -def src(data): - """ - Print the source code URL for a Java class, object, or class name. - - :param data: The Java class, object, or fully qualified class name as string - """ - source_url = jsource(data) - print(f"Source code URL: {source_url}") diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py new file mode 100644 index 0000000..882cd61 --- /dev/null +++ b/src/scyjava/inspect.py @@ -0,0 +1,155 @@ +""" +High-level convenience functions for inspecting Java objects. +""" + +from typing import Optional + +from scyjava._introspect import jreflect, jsource + + +def members(data): + """ + Print all the members (constructors, fields, and methods) + for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="all") + + +def constructors(data): + """ + Print the constructors for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="constructors") + + +def fields(data): + """ + Print the fields for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="fields") + + +def methods(data): + """ + Print the methods for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="methods") + + +def src(data): + """ + Print the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + source_url = jsource(data) + print(f"Source code URL: {source_url}") + + +def _map_syntax(base_type): + """ + Map a Java BaseType annotation (see link below) in an Java array + to a specific type with an Python interpretable syntax. + https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 + """ + basetype_mapping = { + "[B": "byte[]", + "[C": "char[]", + "[D": "double[]", + "[F": "float[]", + "[I": "int[]", + "[J": "long[]", + "[L": "[]", # array + "[S": "short[]", + "[Z": "boolean[]", + } + + if base_type in basetype_mapping: + return basetype_mapping[base_type] + # Handle the case of a returned array of an object + elif base_type.__str__().startswith("[L"): + return base_type.__str__()[2:-1] + "[]" + else: + return base_type + + +def _pretty_string(entry, offset): + """ + Print the entry with a specific formatting and aligned style. + + :param entry: Dictionary of class names, modifiers, arguments, and return values. + :param offset: Offset between the return value and the method. + """ + + # A star implies that the method is a static method + return_type = entry["returns"] or "void" + return_val = f"{return_type.__str__():<{offset}}" + # Handle whether to print static/instance modifiers + obj_name = f"{entry['name']}" + modifier = f"{'*':>4}" if "static" in entry["mods"] else f"{'':>4}" + + # Handle fields + if entry["arguments"] is None: + return f"{return_val} {modifier} = {obj_name}\n" + + # Handle methods with no arguments + if len(entry["arguments"]) == 0: + return f"{return_val} {modifier} = {obj_name}()\n" + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + return f"{return_val} {modifier} = {obj_name}({arg_string})\n" + + +def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): + """ + Write data to a printed table with inputs, static modifier, + arguments, and return values. + + :param data: The object or class to inspect or fully qualified class name. + :param static: + Boolean filter on Static or Instance methods. + Optional, default is None (prints all). + :param source: Whether to print any available source code. Default True. + """ + table = jreflect(data, aspect) + if len(table) == 0: + print(f"No {aspect} found") + return + + # Print source code + offset = max(list(map(lambda entry: len(entry["returns"] or "void"), table))) + all_methods = "" + if source: + urlstring = jsource(data) + print(f"Source code URL: {urlstring}") + + # Print methods + for entry in table: + if entry["returns"]: + entry["returns"] = _map_syntax(entry["returns"]) + if entry["arguments"]: + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + if static is None: + entry_string = _pretty_string(entry, offset) + all_methods += entry_string + + elif static and "static" in entry["mods"]: + entry_string = _pretty_string(entry, offset) + all_methods += entry_string + elif not static and "static" not in entry["mods"]: + entry_string = _pretty_string(entry, offset) + all_methods += entry_string + else: + continue + + # 4 added to align the asterisk with output. + print(f"{'':<{offset + 4}}* indicates static modifier") + print(all_methods) From 30bd4d3a9b5e1535a4124d6310e3bde983823afe Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 20:48:13 -0500 Subject: [PATCH 32/41] Hide non-public scijava.config attrs --- src/scyjava/config.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index e2cc007..712b2ab 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -1,24 +1,26 @@ -import enum -import logging -import os -import pathlib +import enum as _enum +import logging as _logging +import os as _os +import pathlib as _pathlib -import jpype -from jgo import maven_scijava_repository +import jpype as _jpype +from jgo import maven_scijava_repository as _scijava_public -_logger = logging.getLogger(__name__) + +_logger = _logging.getLogger(__name__) endpoints = [] -_repositories = {"scijava.public": maven_scijava_repository()} + +_repositories = {"scijava.public": _scijava_public()} _verbose = 0 _manage_deps = True -_cache_dir = pathlib.Path.home() / ".jgo" -_m2_repo = pathlib.Path.home() / ".m2" / "repository" +_cache_dir = _pathlib.Path.home() / ".jgo" +_m2_repo = _pathlib.Path.home() / ".m2" / "repository" _options = [] _shortcuts = {} -class Mode(enum.Enum): +class Mode(_enum.Enum): JEP = "jep" JPYPE = "jpype" @@ -143,7 +145,7 @@ def add_classpath(*path): foo.bar.Fubar. """ for p in path: - jpype.addClassPath(p) + _jpype.addClassPath(p) def find_jars(directory): @@ -154,16 +156,16 @@ def find_jars(directory): :return: a list of JAR files """ jars = [] - for root, _, files in os.walk(directory): + for root, _, files in _os.walk(directory): for f in files: if f.lower().endswith(".jar"): - path = os.path.join(root, f) + path = _os.path.join(root, f) jars.append(path) return jars def get_classpath(): - return jpype.getClassPath() + return _jpype.getClassPath() def set_heap_min(mb: int = None, gb: int = None): From a537c00f3e9cf04770fff11b02bd69d3b9f5035b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 20:58:24 -0500 Subject: [PATCH 33/41] Hide non-public scyjava.inspect attrs --- src/scyjava/inspect.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 882cd61..2ea01f1 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -2,9 +2,7 @@ High-level convenience functions for inspecting Java objects. """ -from typing import Optional - -from scyjava._introspect import jreflect, jsource +from scyjava import _introspect def members(data): @@ -50,7 +48,7 @@ def src(data): :param data: The Java class, object, or fully qualified class name as string. """ - source_url = jsource(data) + source_url = _introspect.jsource(data) print(f"Source code URL: {source_url}") @@ -108,7 +106,7 @@ def _pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): +def _print_data(data, aspect, static: bool | None = None, source: bool = True): """ Write data to a printed table with inputs, static modifier, arguments, and return values. @@ -119,7 +117,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ - table = jreflect(data, aspect) + table = _introspect.jreflect(data, aspect) if len(table) == 0: print(f"No {aspect} found") return @@ -128,7 +126,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True offset = max(list(map(lambda entry: len(entry["returns"] or "void"), table))) all_methods = "" if source: - urlstring = jsource(data) + urlstring = _introspect.jsource(data) print(f"Source code URL: {urlstring}") # Print methods From cea10cd4322796f09ef885b2dca6fccea996ac86 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 21:02:12 -0500 Subject: [PATCH 34/41] Use jimport naming convention for Java types --- src/scyjava/_introspect.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 067abe7..a00b5b8 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -85,9 +85,9 @@ def jsource(data): The object or class or fully qualified class name to check for source code. :return: The URL of the java class """ - types = jimport("org.scijava.util.Types") - sf = jimport("org.scijava.search.SourceFinder") - jstring = jimport("java.lang.String") + Types = jimport("org.scijava.util.Types") + SourceFinder = jimport("org.scijava.search.SourceFinder") + String = jimport("java.lang.String") try: if not isjava(data) and isinstance(data, str): try: @@ -95,10 +95,10 @@ def jsource(data): except Exception as err: raise ValueError(f"Not a Java object {err}") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - if types.location(jcls).toString().startsWith(jstring("jrt")): + if Types.location(jcls).toString().startsWith(String("jrt")): # Handles Java RunTime (jrt) exceptions. raise ValueError("Java Builtin: GitHub source code not available") - url = sf.sourceLocation(jcls, None) + url = SourceFinder.sourceLocation(jcls, None) urlstring = url.toString() return urlstring except jimport("java.lang.IllegalArgumentException") as err: From 3a46f1bde40d39e3bcb535032dd7aca8de9b1fd1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 21:51:46 -0500 Subject: [PATCH 35/41] Make output writer configurable --- src/scyjava/inspect.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 2ea01f1..13b5f03 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -2,54 +2,62 @@ High-level convenience functions for inspecting Java objects. """ +from sys import stdout as _stdout + from scyjava import _introspect -def members(data): +def members(data, writer=None): """ Print all the members (constructors, fields, and methods) for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="all") + _print_data(data, aspect="all", writer=writer) -def constructors(data): +def constructors(data, writer=None): """ Print the constructors for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="constructors") + _print_data(data, aspect="constructors", writer=writer) -def fields(data): +def fields(data, writer=None): """ Print the fields for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="fields") + _print_data(data, aspect="fields", writer=writer) -def methods(data): +def methods(data, writer=None): """ Print the methods for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ _print_data(data, aspect="methods") -def src(data): +def src(data, writer=None): """ Print the source code URL for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ + writer = writer or _stdout.write source_url = _introspect.jsource(data) - print(f"Source code URL: {source_url}") + writer(f"Source code URL: {source_url}\n") def _map_syntax(base_type): @@ -106,7 +114,9 @@ def _pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def _print_data(data, aspect, static: bool | None = None, source: bool = True): +def _print_data( + data, aspect, static: bool | None = None, source: bool = True, writer=None +): """ Write data to a printed table with inputs, static modifier, arguments, and return values. @@ -117,9 +127,10 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ + writer = writer or _stdout.write table = _introspect.jreflect(data, aspect) if len(table) == 0: - print(f"No {aspect} found") + writer(f"No {aspect} found\n") return # Print source code @@ -127,7 +138,7 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): all_methods = "" if source: urlstring = _introspect.jsource(data) - print(f"Source code URL: {urlstring}") + writer(f"Source code URL: {urlstring}\n") # Print methods for entry in table: @@ -147,7 +158,8 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): all_methods += entry_string else: continue + all_methods += "\n" # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates static modifier") - print(all_methods) + writer(f"{'':<{offset + 4}}* indicates static modifier\n") + writer(all_methods) From 5fe146166d52ae4082e1f22491deac6603d023af Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 21:53:46 -0500 Subject: [PATCH 36/41] Replace print statements with logger calls --- src/scyjava/_jvm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 1a6c5ca..742b6eb 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -226,7 +226,7 @@ def shutdown_jvm() -> None: try: callback() except Exception as e: - print(f"Exception during shutdown callback: {e}") + _logger.error(f"Exception during shutdown callback: {e}") # dispose AWT resources if applicable if is_awt_initialized(): @@ -238,7 +238,7 @@ def shutdown_jvm() -> None: try: jpype.shutdownJVM() except Exception as e: - print(f"Exception during JVM shutdown: {e}") + _logger.error(f"Exception during JVM shutdown: {e}") def jvm_started() -> bool: From 2f11f0d8c5efb7719448c58cfe42b6ee372b89f0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 22:35:31 -0500 Subject: [PATCH 37/41] Add unit test for inspect.members function --- tests/test_inspect.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/test_inspect.py diff --git a/tests/test_inspect.py b/tests/test_inspect.py new file mode 100644 index 0000000..c99076d --- /dev/null +++ b/tests/test_inspect.py @@ -0,0 +1,29 @@ +""" +Tests for functions in inspect submodule. +""" + +from scyjava import inspect +from scyjava.config import mode, Mode + + +class TestInspect(object): + """ + Test scyjava.inspect convenience functions. + """ + + def test_inspect_members(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + members = [] + inspect.members("java.lang.Iterable", writer=members.append) + expected = [ + "Source code URL: java.lang.NullPointerException", + " * indicates static modifier", + "java.util.Iterator = iterator()", + "java.util.Spliterator = spliterator()", + "void = forEach(java.util.function.Consumer)", + "", + "", + ] + assert expected == "".join(members).split("\n") From f4266c40fa2c3dc8940b185fd72776bc39ab6734 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 22:45:36 -0500 Subject: [PATCH 38/41] Ensure submodules are directly available This code worked: import scyjava print(scyjava.config) But this code didn't: import scyjava print(scyjava.inspect) Because scyjava.config was being imported in another file further down the chain. Better to be explicit about wanting both of these submodules available at the top level. --- src/scyjava/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 4f7c6a2..e19cc79 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -71,6 +71,7 @@ from functools import lru_cache from typing import Any, Callable, Dict +from . import config, inspect from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike from ._convert import ( Converter, From 1d9b507d0b51932a21af44d6e869411e8efb2923 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 25 Apr 2025 18:07:37 -0500 Subject: [PATCH 39/41] Let jsource also find Java library source code And do not try so hard with exception handling; it should be up to higher level functions like scyjava.inspect.src to catch such failures. --- src/scyjava/_introspect.py | 71 +++++++++++++++++++++++--------------- tests/test_inspect.py | 6 ++-- tests/test_introspect.py | 11 ++++++ 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index a00b5b8..9449fc8 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -5,7 +5,7 @@ class methods, fields, and source code URL. from typing import Any, Dict, List -from scyjava._jvm import jimport +from scyjava._jvm import jimport, jvm_version from scyjava._types import isjava, jinstance, jclass @@ -78,34 +78,49 @@ def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: return table -def jsource(data): +def jsource(data) -> str: """ - Try to find the source code using SciJava's SourceFinder. + Try to find the source code URL for the given Java object, class, or class name. + Requires org.scijava:scijava-search on the classpath. :param data: - The object or class or fully qualified class name to check for source code. - :return: The URL of the java class + Object, class, or fully qualified class name for which to discern the source code location. + :return: URL of the class's source code. """ - Types = jimport("org.scijava.util.Types") + + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) # check if data can be imported + except Exception as err: + raise ValueError(f"Not a Java object {err}") + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) + + if jcls.getClassLoader() is None: + # Class is from the Java standard library. + cls_path = str(jcls.getName()).replace(".", "/") + + # Discern the Java version. + java_version = jvm_version()[0] + + # Note: some classes (e.g. corba and jaxp) will not be located correctly before + # Java 10, because they fall under a different subtree than `jdk`. But Java 11+ + # dispenses with such subtrees in favor of using only the module designations. + if java_version <= 7: + return f"https://github.com/openjdk/jdk/blob/jdk7-b147/jdk/src/share/classes/{cls_path}.java" + elif java_version == 8: + return f"https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/classes/{cls_path}.java" + else: # java_version >= 9 + module_name = jcls.getModule().getName() + # if module_name is null, it's in the unnamed module + if java_version == 9: + suffix = "%2B181/jdk" + elif java_version == 10: + suffix = "%2B46" + else: + suffix = "-ga" + return f"https://github.com/openjdk/jdk/blob/jdk-{java_version}{suffix}/src/{module_name}/share/classes/{cls_path}.java" + + # Ask scijava-search for the source location. SourceFinder = jimport("org.scijava.search.SourceFinder") - String = jimport("java.lang.String") - try: - if not isjava(data) and isinstance(data, str): - try: - data = jimport(data) # check if data can be imported - except Exception as err: - raise ValueError(f"Not a Java object {err}") - jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - if Types.location(jcls).toString().startsWith(String("jrt")): - # Handles Java RunTime (jrt) exceptions. - raise ValueError("Java Builtin: GitHub source code not available") - url = SourceFinder.sourceLocation(jcls, None) - urlstring = url.toString() - return urlstring - except jimport("java.lang.IllegalArgumentException") as err: - return f"Illegal argument provided {err=}, {type(err)=}" - except ValueError as err: - return f"{err}" - except TypeError: - return f"Not a Java class {str(type(data))}" - except Exception as err: - return f"Unexpected {err=}, {type(err)=}" + url = SourceFinder.sourceLocation(jcls, None) + urlstring = url.toString() + return urlstring diff --git a/tests/test_inspect.py b/tests/test_inspect.py index c99076d..4eca8d9 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -18,7 +18,9 @@ def test_inspect_members(self): members = [] inspect.members("java.lang.Iterable", writer=members.append) expected = [ - "Source code URL: java.lang.NullPointerException", + "Source code URL: " + "https://github.com/openjdk/jdk/blob/jdk-11-ga/" + "src/java.base/share/classes/java/lang/Iterable.java", " * indicates static modifier", "java.util.Iterator = iterator()", "java.util.Spliterator = spliterator()", @@ -26,4 +28,4 @@ def test_inspect_members(self): "", "", ] - assert expected == "".join(members).split("\n") + assert "".join(members).split("\n") == expected diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 09eb692..bbfa93b 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -57,6 +57,17 @@ def test_jsource(self): assert source_SF.startsWith(repo_path) assert source_strSF == source_SF + def test_jsource_jdk_class(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + jv = scyjava.jvm_version()[0] + source = scyjava.jsource("java.util.List") + assert ( + source == f"https://github.com/openjdk/jdk/blob/jdk-{jv}-ga/" + "src/java.base/share/classes/java/util/List.java" + ) + def test_imagej_legacy(self): if mode == Mode.JEP: # JEP does not support the jclass function. From 5c067473145d6cc49ba5255b3738a07c2b3d8bb3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 25 Apr 2025 18:09:38 -0500 Subject: [PATCH 40/41] Be less aggressive with source code detection When invoking scyjava.inspect functions, they can optionally report the source URL at the top, before printing the members. But this only works if scijava-search is on the classpath. Let's let the source flag default to None, in which case it swallows source code URL detection failures gracefully, to make the common case of scijava-search not being available work without hassle. And let's have the various inspect functions accept static and source boolean flags, which get passed along to the internal _print_data routine, as was previously the case when they were partial functions. --- src/scyjava/inspect.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 13b5f03..6b0d9e8 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -7,7 +7,7 @@ from scyjava import _introspect -def members(data, writer=None): +def members(data, static: bool | None = None, source: bool | None = None, writer=None): """ Print all the members (constructors, fields, and methods) for a Java class, object, or class name. @@ -15,30 +15,34 @@ def members(data, writer=None): :param data: The Java class, object, or fully qualified class name as string. :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="all", writer=writer) + _print_data(data, aspect="all", static=static, source=source, writer=writer) -def constructors(data, writer=None): +def constructors( + data, static: bool | None = None, source: bool | None = None, writer=None +): """ Print the constructors for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="constructors", writer=writer) + _print_data( + data, aspect="constructors", static=static, source=source, writer=writer + ) -def fields(data, writer=None): +def fields(data, static: bool | None = None, source: bool | None = None, writer=None): """ Print the fields for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="fields", writer=writer) + _print_data(data, aspect="fields", static=static, source=source, writer=writer) -def methods(data, writer=None): +def methods(data, static: bool | None = None, source: bool | None = None, writer=None): """ Print the methods for a Java class, object, or class name. @@ -115,7 +119,7 @@ def _pretty_string(entry, offset): def _print_data( - data, aspect, static: bool | None = None, source: bool = True, writer=None + data, aspect, static: bool | None = None, source: bool | None = None, writer=None ): """ Write data to a printed table with inputs, static modifier, @@ -125,7 +129,11 @@ def _print_data( :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). - :param source: Whether to print any available source code. Default True. + :param source: + Whether to discern and report a URL to the relevant source code. + Requires org.scijava:scijava-search to be on the classpath. + When set to None (the default), autodetects whether scijava-search + is available, reporting source URL if so, or leaving it out if not. """ writer = writer or _stdout.write table = _introspect.jreflect(data, aspect) @@ -136,9 +144,15 @@ def _print_data( # Print source code offset = max(list(map(lambda entry: len(entry["returns"] or "void"), table))) all_methods = "" - if source: - urlstring = _introspect.jsource(data) - writer(f"Source code URL: {urlstring}\n") + if source or source is None: + try: + urlstring = _introspect.jsource(data) + writer(f"Source code URL: {urlstring}\n") + except TypeError: + if source: + writer( + "Classpath lacks scijava-search; no source code URL detection is available.\n" + ) # Print methods for entry in table: From e5e9cab0184ccfb09b96c30469bbca6b78fc8295 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 13:25:33 -0500 Subject: [PATCH 41/41] Add test for jreflect constructors --- tests/test_introspect.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/test_introspect.py b/tests/test_introspect.py index bbfa93b..7c7fe99 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -39,11 +39,46 @@ def test_jreflect_fields(self): BitSet = scyjava.jimport(str_BitSet) str_Obj = scyjava.jreflect(str_BitSet, "fields") bitset_Obj = scyjava.jreflect(BitSet, "fields") - assert len(str_Obj) == 0 - assert len(bitset_Obj) == 0 + assert len(str_Obj) == len(bitset_Obj) == 0 assert bitset_Obj is not None assert bitset_Obj == str_Obj + def test_jreflect_ctors(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_ArrayList = "java.util.ArrayList" + ArrayList = scyjava.jimport(str_ArrayList) + str_Obj = scyjava.jreflect(str_ArrayList, "constructors") + arraylist_Obj = scyjava.jreflect(ArrayList, "constructors") + assert len(str_Obj) == len(arraylist_Obj) == 3 + arraylist_Obj.sort( + key=lambda row: f"{row['type']}:{row['name']}:{','.join(str(row['arguments']))}" + ) + assert arraylist_Obj == [ + { + "arguments": ["int"], + "mods": ["public"], + "name": "java.util.ArrayList", + "returns": "java.util.ArrayList", + "type": "constructor", + }, + { + "arguments": ["java.util.Collection"], + "mods": ["public"], + "name": "java.util.ArrayList", + "returns": "java.util.ArrayList", + "type": "constructor", + }, + { + "arguments": [], + "mods": ["public"], + "name": "java.util.ArrayList", + "returns": "java.util.ArrayList", + "type": "constructor", + }, + ] + def test_jsource(self): if mode == Mode.JEP: # JEP does not support the jclass function.