Skip to content

Commit f83edfa

Browse files
committed
Merge branch 'object-introspection'
See PR #78.
2 parents 5753a59 + e5e9cab commit f83edfa

16 files changed

+573
-43
lines changed

README.md

+31-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ AttributeError: 'list' object has no attribute 'stream'
135135
Traceback (most recent call last):
136136
File "<stdin>", line 1, in <module>
137137
TypeError: No matching overloads found for java.util.Set.addAll(set), options are:
138-
public abstract boolean java.util.Set.addAll(java.util.Collection)
138+
public abstract boolean java.util.Set.addAll(java.util.Collection)
139139
>>> from scyjava import to_java as p2j
140140
>>> jset.addAll(p2j(pset))
141141
True
@@ -216,6 +216,22 @@ FUNCTIONS
216216
is_jarray(data: Any) -> bool
217217
Return whether the given data object is a Java array.
218218

219+
is_jboolean(the_type: type) -> bool
220+
221+
is_jbyte(the_type: type) -> bool
222+
223+
is_jcharacter(the_type: type) -> bool
224+
225+
is_jdouble(the_type: type) -> bool
226+
227+
is_jfloat(the_type: type) -> bool
228+
229+
is_jinteger(the_type: type) -> bool
230+
231+
is_jlong(the_type: type) -> bool
232+
233+
is_jshort(the_type: type) -> bool
234+
219235
is_jvm_headless() -> bool
220236
Return true iff Java is running in headless mode.
221237

@@ -267,6 +283,12 @@ FUNCTIONS
267283
You can pass a single integer to make a 1-dimensional array of that length.
268284
:return: The newly allocated array
269285

286+
jsource(data)
287+
Try to find the source code using SciJava's SourceFinder.
288+
:param data:
289+
The object or class or fully qualified class name to check for source code.
290+
:return: The URL of the java class
291+
270292
jclass(data)
271293
Obtain a Java class object.
272294

@@ -303,6 +325,14 @@ FUNCTIONS
303325
:param jtype: The Java type, as either a jimported class or as a string.
304326
:return: True iff the object is an instance of that Java type.
305327

328+
jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]
329+
Use Java reflection to introspect the given Java object,
330+
returning a table of its available methods or fields.
331+
332+
:param data: The object or class or fully qualified class name to inspect.
333+
:param aspect: One of: "all", "constructors", "fields", or "methods".
334+
:return: List of dicts with keys: "name", "mods", "arguments", and "returns".
335+
306336
jstacktrace(exc) -> str
307337
Extract the Java-side stack trace from a Java exception.
308338

src/scyjava/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
from functools import lru_cache
7272
from typing import Any, Callable, Dict
7373

74+
from . import config, inspect
7475
from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike
7576
from ._convert import (
7677
Converter,
@@ -91,6 +92,10 @@
9192
to_java,
9293
to_python,
9394
)
95+
from ._introspect import (
96+
jreflect,
97+
jsource,
98+
)
9499
from ._jvm import ( # noqa: F401
95100
available_processors,
96101
gc,

src/scyjava/_introspect.py

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
Introspection functions for reporting Java
3+
class methods, fields, and source code URL.
4+
"""
5+
6+
from typing import Any, Dict, List
7+
8+
from scyjava._jvm import jimport, jvm_version
9+
from scyjava._types import isjava, jinstance, jclass
10+
11+
12+
def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]:
13+
"""
14+
Use Java reflection to introspect the given Java object,
15+
returning a table of its available methods or fields.
16+
17+
:param data: The object or class or fully qualified class name to inspect.
18+
:param aspect: One of: "all", "constructors", "fields", or "methods".
19+
:return: List of dicts with keys: "name", "mods", "arguments", and "returns".
20+
"""
21+
22+
aspects = ["all", "constructors", "fields", "methods"]
23+
if aspect not in aspects:
24+
raise ValueError("aspect must be one of {aspects}")
25+
26+
if not isjava(data) and isinstance(data, str):
27+
try:
28+
data = jimport(data)
29+
except Exception as e:
30+
raise ValueError(
31+
f"Object of type '{type(data).__name__}' is not a Java object"
32+
) from e
33+
34+
jcls = data if jinstance(data, "java.lang.Class") else jclass(data)
35+
36+
Modifier = jimport("java.lang.reflect.Modifier")
37+
modifiers = {
38+
attr[2:].lower(): getattr(Modifier, attr)
39+
for attr in dir(Modifier)
40+
if attr.startswith("is")
41+
}
42+
43+
members = []
44+
if aspect in ["all", "constructors"]:
45+
members.extend(jcls.getConstructors())
46+
if aspect in ["all", "fields"]:
47+
members.extend(jcls.getFields())
48+
if aspect in ["all", "methods"]:
49+
members.extend(jcls.getMethods())
50+
51+
table = []
52+
53+
for member in members:
54+
mtype = str(member.getClass().getName()).split(".")[-1].lower()
55+
name = member.getName()
56+
modflags = member.getModifiers()
57+
mods = [name for name, hasmod in modifiers.items() if hasmod(modflags)]
58+
args = (
59+
[ptype.getName() for ptype in member.getParameterTypes()]
60+
if hasattr(member, "getParameterTypes")
61+
else None
62+
)
63+
returns = (
64+
member.getReturnType().getName()
65+
if hasattr(member, "getReturnType")
66+
else (member.getType().getName() if hasattr(member, "getType") else name)
67+
)
68+
table.append(
69+
{
70+
"type": mtype,
71+
"name": name,
72+
"mods": mods,
73+
"arguments": args,
74+
"returns": returns,
75+
}
76+
)
77+
78+
return table
79+
80+
81+
def jsource(data) -> str:
82+
"""
83+
Try to find the source code URL for the given Java object, class, or class name.
84+
Requires org.scijava:scijava-search on the classpath.
85+
:param data:
86+
Object, class, or fully qualified class name for which to discern the source code location.
87+
:return: URL of the class's source code.
88+
"""
89+
90+
if not isjava(data) and isinstance(data, str):
91+
try:
92+
data = jimport(data) # check if data can be imported
93+
except Exception as err:
94+
raise ValueError(f"Not a Java object {err}")
95+
jcls = data if jinstance(data, "java.lang.Class") else jclass(data)
96+
97+
if jcls.getClassLoader() is None:
98+
# Class is from the Java standard library.
99+
cls_path = str(jcls.getName()).replace(".", "/")
100+
101+
# Discern the Java version.
102+
java_version = jvm_version()[0]
103+
104+
# Note: some classes (e.g. corba and jaxp) will not be located correctly before
105+
# Java 10, because they fall under a different subtree than `jdk`. But Java 11+
106+
# dispenses with such subtrees in favor of using only the module designations.
107+
if java_version <= 7:
108+
return f"https://github.com/openjdk/jdk/blob/jdk7-b147/jdk/src/share/classes/{cls_path}.java"
109+
elif java_version == 8:
110+
return f"https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/classes/{cls_path}.java"
111+
else: # java_version >= 9
112+
module_name = jcls.getModule().getName()
113+
# if module_name is null, it's in the unnamed module
114+
if java_version == 9:
115+
suffix = "%2B181/jdk"
116+
elif java_version == 10:
117+
suffix = "%2B46"
118+
else:
119+
suffix = "-ga"
120+
return f"https://github.com/openjdk/jdk/blob/jdk-{java_version}{suffix}/src/{module_name}/share/classes/{cls_path}.java"
121+
122+
# Ask scijava-search for the source location.
123+
SourceFinder = jimport("org.scijava.search.SourceFinder")
124+
url = SourceFinder.sourceLocation(jcls, None)
125+
urlstring = url.toString()
126+
return urlstring

src/scyjava/_jvm.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def shutdown_jvm() -> None:
244244
try:
245245
callback()
246246
except Exception as e:
247-
print(f"Exception during shutdown callback: {e}")
247+
_logger.error(f"Exception during shutdown callback: {e}")
248248

249249
# dispose AWT resources if applicable
250250
if is_awt_initialized():
@@ -256,7 +256,7 @@ def shutdown_jvm() -> None:
256256
try:
257257
jpype.shutdownJVM()
258258
except Exception as e:
259-
print(f"Exception during JVM shutdown: {e}")
259+
_logger.error(f"Exception during JVM shutdown: {e}")
260260

261261

262262
def jvm_started() -> bool:

src/scyjava/_versions.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def get_version(java_class_or_python_package) -> str:
1515
"""
1616
Return the version of a Java class or Python package.
1717
18-
For Python package, uses importlib.metadata.version if available
19-
(Python 3.8+), with pkg_resources.get_distribution as a fallback.
18+
For Python packages, invokes importlib.metadata.version on the given
19+
object's base __module__ or __package__ (before the first dot symbol).
2020
2121
For Java classes, requires org.scijava:scijava-common on the classpath.
2222
@@ -32,8 +32,16 @@ def get_version(java_class_or_python_package) -> str:
3232
VersionUtils = jimport("org.scijava.util.VersionUtils")
3333
return str(VersionUtils.getVersion(java_class_or_python_package))
3434

35-
# Assume we were given a Python package name.
36-
return version(java_class_or_python_package)
35+
# Assume we were given a Python package name or module.
36+
package_name = None
37+
if hasattr(java_class_or_python_package, "__module__"):
38+
package_name = java_class_or_python_package.__module__
39+
elif hasattr(java_class_or_python_package, "__package__"):
40+
package_name = java_class_or_python_package.__package__
41+
else:
42+
package_name = str(java_class_or_python_package)
43+
44+
return version(package_name.split(".")[0])
3745

3846

3947
def is_version_at_least(actual_version: str, minimum_version: str) -> bool:

src/scyjava/config.py

+17-15
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
import enum
2-
import logging
3-
import os
4-
import pathlib
1+
import enum as _enum
2+
import logging as _logging
3+
import os as _os
4+
import pathlib as _pathlib
55

6-
import jpype
7-
from jgo import maven_scijava_repository
6+
import jpype as _jpype
7+
from jgo import maven_scijava_repository as _scijava_public
88

9-
_logger = logging.getLogger(__name__)
9+
10+
_logger = _logging.getLogger(__name__)
1011

1112
endpoints = []
12-
_repositories = {"scijava.public": maven_scijava_repository()}
13+
14+
_repositories = {"scijava.public": _scijava_public()}
1315
_verbose = 0
1416
_manage_deps = True
15-
_cache_dir = pathlib.Path.home() / ".jgo"
16-
_m2_repo = pathlib.Path.home() / ".m2" / "repository"
17+
_cache_dir = _pathlib.Path.home() / ".jgo"
18+
_m2_repo = _pathlib.Path.home() / ".m2" / "repository"
1719
_options = []
1820
_shortcuts = {}
1921

2022

21-
class Mode(enum.Enum):
23+
class Mode(_enum.Enum):
2224
JEP = "jep"
2325
JPYPE = "jpype"
2426

@@ -143,7 +145,7 @@ def add_classpath(*path):
143145
foo.bar.Fubar.
144146
"""
145147
for p in path:
146-
jpype.addClassPath(p)
148+
_jpype.addClassPath(p)
147149

148150

149151
def find_jars(directory):
@@ -154,16 +156,16 @@ def find_jars(directory):
154156
:return: a list of JAR files
155157
"""
156158
jars = []
157-
for root, _, files in os.walk(directory):
159+
for root, _, files in _os.walk(directory):
158160
for f in files:
159161
if f.lower().endswith(".jar"):
160-
path = os.path.join(root, f)
162+
path = _os.path.join(root, f)
161163
jars.append(path)
162164
return jars
163165

164166

165167
def get_classpath():
166-
return jpype.getClassPath()
168+
return _jpype.getClassPath()
167169

168170

169171
def set_heap_min(mb: int = None, gb: int = None):

0 commit comments

Comments
 (0)