Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add object introspection functions #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0a8141c
Add some functions for Java object introspection
ctrueden Mar 26, 2025
7b8a6c4
Update methods() functionality
ian-coccimiglio Mar 27, 2025
fe0bfe3
Make progress on introspection methods
ian-coccimiglio Mar 27, 2025
25d769e
Make linter happy
ian-coccimiglio Mar 27, 2025
1a0fd71
Add source code reporting to methods() function
ian-coccimiglio Mar 27, 2025
640d57d
Implement fields introspection function
ian-coccimiglio Mar 27, 2025
aa3996c
Add partials, refactor, add java_source function
ian-coccimiglio Mar 28, 2025
9352ff6
Refactor introspection code
ian-coccimiglio Mar 28, 2025
fe60217
Add test cases for introspection functions
ian-coccimiglio Mar 28, 2025
7293d23
Lint code
ian-coccimiglio Mar 28, 2025
4123078
Improve introspection function documentation
ian-coccimiglio Mar 31, 2025
3533446
Add docstring to test_introspection.py
ian-coccimiglio Mar 31, 2025
9516a72
Wrap long line
ctrueden Apr 2, 2025
6520223
Increment minor version digit
ctrueden Apr 2, 2025
363e7bc
Alphabetize introspection imports
ctrueden Apr 2, 2025
c8afba4
Shorten introspection to introspect
ctrueden Apr 2, 2025
6bfec82
Add toplevel docstrings to test files
ctrueden Apr 2, 2025
37bd92e
Fix naming of versions test file
ctrueden Apr 2, 2025
5f883f1
Fix type hints to work with Python 3.8
ctrueden Apr 2, 2025
6282d8c
CI: test Python 3.13 support
ctrueden Apr 2, 2025
bded14f
Rename find_java function to jreflect
ctrueden Apr 2, 2025
0778a89
Add missing is_j* type methods to README
ctrueden Apr 2, 2025
21ffae9
Use imperative tense for function docstrings
ctrueden Apr 2, 2025
bb06ed1
Wrap >88 lines, and make quoting more consistent
ctrueden Apr 2, 2025
553d552
Add introspection functions to the README
ctrueden Apr 2, 2025
bf2ee82
Improve get_version method
ctrueden Apr 24, 2025
2d44fed
Test a little further into the GitHub source paths
ctrueden Apr 24, 2025
8846ce5
Tweak management of multiple endpoints
ctrueden Apr 24, 2025
a31701b
Rename java_source method to jsource
ctrueden Apr 24, 2025
2713b6c
Make jreflect function more powerful
ctrueden Apr 24, 2025
92d7fb1
Split pretty-print functions to own subpackage
ctrueden Apr 25, 2025
30bd4d3
Hide non-public scijava.config attrs
ctrueden Apr 25, 2025
a537c00
Hide non-public scyjava.inspect attrs
ctrueden Apr 25, 2025
cea10cd
Use jimport naming convention for Java types
ctrueden Apr 25, 2025
3a46f1b
Make output writer configurable
ctrueden Apr 25, 2025
5fe1461
Replace print statements with logger calls
ctrueden Apr 25, 2025
2f11f0d
Add unit test for inspect.members function
ctrueden Apr 25, 2025
f4266c4
Ensure submodules are directly available
ctrueden Apr 25, 2025
1d9b507
Let jsource also find Java library source code
ctrueden Apr 25, 2025
5c06747
Be less aggressive with source code detection
ctrueden Apr 25, 2025
e5e9cab
Add test for jreflect constructors
ctrueden Apr 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
python-version: [
'3.8',
'3.10',
'3.12'
'3.13'
]

steps:
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ AttributeError: 'list' object has no attribute 'stream'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
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
Expand Down Expand Up @@ -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.

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

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.
:return: The URL of the java class

jclass(data)
Obtain a Java class object.

Expand Down Expand Up @@ -303,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 = "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: 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.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]"}]
Expand Down
5 changes: 5 additions & 0 deletions src/scyjava/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -91,6 +92,10 @@
to_java,
to_python,
)
from ._introspect import (
jreflect,
jsource,
)
from ._jvm import ( # noqa: F401
available_processors,
gc,
Expand Down
126 changes: 126 additions & 0 deletions src/scyjava/_introspect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""
Introspection functions for reporting Java
class methods, fields, and source code URL.
"""

from typing import Any, Dict, List

from scyjava._jvm import jimport, jvm_version
from scyjava._types import isjava, jinstance, jclass


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: 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 e:
raise ValueError(
f"Object of type '{type(data).__name__}' is not a Java object"
) from e

jcls = data if jinstance(data, "java.lang.Class") else jclass(data)

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 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 name)
)
table.append(
{
"type": mtype,
"name": name,
"mods": mods,
"arguments": args,
"returns": returns,
}
)

return table


def jsource(data) -> str:
"""
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:
Object, class, or fully qualified class name for which to discern the source code location.
:return: URL of the class's source code.
"""

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")
url = SourceFinder.sourceLocation(jcls, None)
urlstring = url.toString()
return urlstring
4 changes: 2 additions & 2 deletions src/scyjava/_jvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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:
Expand Down
16 changes: 12 additions & 4 deletions src/scyjava/_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:
Expand Down
32 changes: 17 additions & 15 deletions src/scyjava/config.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
Loading