Skip to content

Commit 9352ff6

Browse files
ian-coccimiglioctrueden
authored andcommitted
Refactor introspection code
1 parent aa3996c commit 9352ff6

File tree

3 files changed

+206
-196
lines changed

3 files changed

+206
-196
lines changed

src/scyjava/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,16 @@
124124
jclass,
125125
jinstance,
126126
jstacktrace,
127+
numeric_bounds,
128+
)
129+
from ._introspection import (
127130
find_java,
128131
java_source,
129132
methods,
130133
fields,
131134
attrs,
132135
src,
133136
java_source,
134-
numeric_bounds,
135137
)
136138
from ._versions import compare_version, get_version, is_version_at_least
137139

src/scyjava/_introspection.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""
2+
Introspection functions for reporting java classes and URL
3+
"""
4+
5+
from functools import partial
6+
from typing import Any
7+
8+
from scyjava._jvm import jimport
9+
from scyjava._types import isjava, jinstance, jclass
10+
11+
12+
def find_java(data, aspect: str) -> list[dict[str, Any]]:
13+
"""
14+
Use Java reflection to introspect the given Java object,
15+
returning a table of its available methods.
16+
17+
:param data: The object or class or fully qualified class name to inspect.
18+
:param aspect: Either 'methods' or 'fields'
19+
:return: List of dicts with keys: "name", "static", "arguments", and "returns".
20+
"""
21+
22+
if not isjava(data) and isinstance(data, str):
23+
try:
24+
data = jimport(data)
25+
except:
26+
raise ValueError("Not a Java object")
27+
28+
Modifier = jimport("java.lang.reflect.Modifier")
29+
jcls = data if jinstance(data, "java.lang.Class") else jclass(data)
30+
31+
if aspect == "methods":
32+
cls_aspects = jcls.getMethods()
33+
elif aspect == "fields":
34+
cls_aspects = jcls.getFields()
35+
else:
36+
return "`aspect` must be either 'fields' or 'methods'"
37+
38+
table = []
39+
40+
for m in cls_aspects:
41+
name = m.getName()
42+
if aspect == "methods":
43+
args = [c.getName() for c in m.getParameterTypes()]
44+
returns = m.getReturnType().getName()
45+
elif aspect == "fields":
46+
args = None
47+
returns = m.getType().getName()
48+
mods = Modifier.isStatic(m.getModifiers())
49+
table.append(
50+
{
51+
"name": name,
52+
"static": mods,
53+
"arguments": args,
54+
"returns": returns,
55+
}
56+
)
57+
sorted_table = sorted(table, key=lambda d: d["name"])
58+
59+
return sorted_table
60+
61+
62+
def _map_syntax(base_type):
63+
"""
64+
Maps a java BaseType annotation (see link below) in an Java array
65+
to a specific type with an Python interpretable syntax.
66+
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3
67+
"""
68+
basetype_mapping = {
69+
"[B": "byte[]",
70+
"[C": "char[]",
71+
"[D": "double[]",
72+
"[F": "float[]",
73+
"[I": "int[]",
74+
"[J": "long[]",
75+
"[L": "[]", # array
76+
"[S": "short[]",
77+
"[Z": "boolean[]",
78+
}
79+
80+
if base_type in basetype_mapping:
81+
return basetype_mapping[base_type]
82+
# Handle the case of a returned array of an object
83+
elif base_type.__str__().startswith("[L"):
84+
return base_type.__str__()[2:-1] + "[]"
85+
else:
86+
return base_type
87+
88+
89+
def _make_pretty_string(entry, offset):
90+
"""
91+
Prints the entry with a specific formatting and aligned style
92+
:param entry: Dictionary of class names, modifiers, arguments, and return values.
93+
:param offset: Offset between the return value and the method.
94+
"""
95+
96+
# A star implies that the method is a static method
97+
return_val = f"{entry['returns'].__str__():<{offset}}"
98+
# Handle whether to print static/instance modifiers
99+
obj_name = f"{entry['name']}"
100+
modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}"
101+
102+
# Handle fields
103+
if entry["arguments"] is None:
104+
return f"{return_val} {modifier} = {obj_name}\n"
105+
106+
# Handle methods with no arguments
107+
if len(entry["arguments"]) == 0:
108+
return f"{return_val} {modifier} = {obj_name}()\n"
109+
else:
110+
arg_string = ", ".join([r.__str__() for r in entry["arguments"]])
111+
return f"{return_val} {modifier} = {obj_name}({arg_string})\n"
112+
113+
114+
def java_source(data):
115+
"""
116+
Tries to find the source code using Scijava's SourceFinder'
117+
:param data: The object or class or fully qualified class name to check for source code.
118+
:return: The URL of the java class
119+
"""
120+
types = jimport("org.scijava.util.Types")
121+
sf = jimport("org.scijava.search.SourceFinder")
122+
jstring = jimport("java.lang.String")
123+
try:
124+
if not isjava(data) and isinstance(data, str):
125+
try:
126+
data = jimport(data) # check if data can be imported
127+
except:
128+
raise ValueError("Not a Java object")
129+
jcls = data if jinstance(data, "java.lang.Class") else jclass(data)
130+
if types.location(jcls).toString().startsWith(jstring("jrt")):
131+
# Handles Java RunTime (jrt) exceptions.
132+
raise ValueError("Java Builtin: GitHub source code not available")
133+
url = sf.sourceLocation(jcls, None)
134+
urlstring = url.toString()
135+
return urlstring
136+
except jimport("java.lang.IllegalArgumentException") as err:
137+
return f"Illegal argument provided {err=}, {type(err)=}"
138+
except ValueError as err:
139+
return f"{err}"
140+
except TypeError:
141+
return f"Not a Java class {str(type(data))}"
142+
except Exception as err:
143+
return f"Unexpected {err=}, {type(err)=}"
144+
145+
146+
def _print_data(data, aspect, static: bool | None = None, source: bool = True):
147+
"""
148+
Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values.
149+
150+
:param data: The object or class to inspect.
151+
:param aspect: Whether to print class fields or methods.
152+
:param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on
153+
static vs. instance methods. Optional, default is None (prints all).
154+
:param source: Whether to print any available source code. Default True.
155+
"""
156+
table = find_java(data, aspect)
157+
if len(table) == 0:
158+
print(f"No {aspect} found")
159+
return
160+
161+
# Print source code
162+
offset = max(list(map(lambda entry: len(entry["returns"]), table)))
163+
all_methods = ""
164+
if source:
165+
urlstring = java_source(data)
166+
print(f"Source code URL: {urlstring}")
167+
168+
# Print methods
169+
for entry in table:
170+
entry["returns"] = _map_syntax(entry["returns"])
171+
if entry["arguments"]:
172+
entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]]
173+
if static is None:
174+
entry_string = _make_pretty_string(entry, offset)
175+
all_methods += entry_string
176+
177+
elif static and entry["static"]:
178+
entry_string = _make_pretty_string(entry, offset)
179+
all_methods += entry_string
180+
elif not static and not entry["static"]:
181+
entry_string = _make_pretty_string(entry, offset)
182+
all_methods += entry_string
183+
else:
184+
continue
185+
186+
# 4 added to align the asterisk with output.
187+
print(f"{'':<{offset + 4}}* indicates static modifier")
188+
print(all_methods)
189+
190+
191+
methods = partial(_print_data, aspect="methods")
192+
fields = partial(_print_data, aspect="fields")
193+
attrs = partial(_print_data, aspect="fields")
194+
195+
196+
def src(data):
197+
"""
198+
Prints the source code URL for a Java class, object, or class name.
199+
200+
:param data: The Java class, object, or fully qualified class name as string
201+
"""
202+
source_url = java_source(data)
203+
print(f"Source code URL: {source_url}")

0 commit comments

Comments
 (0)