scyjava 1.10.2__tar.gz → 1.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {scyjava-1.10.2/src/scyjava.egg-info → scyjava-1.12.0}/PKG-INFO +37 -7
  2. {scyjava-1.10.2 → scyjava-1.12.0}/README.md +31 -1
  3. {scyjava-1.10.2 → scyjava-1.12.0}/bin/test.sh +6 -1
  4. {scyjava-1.10.2 → scyjava-1.12.0}/dev-environment.yml +3 -3
  5. {scyjava-1.10.2 → scyjava-1.12.0}/environment.yml +3 -3
  6. {scyjava-1.10.2 → scyjava-1.12.0}/pyproject.toml +6 -6
  7. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava/__init__.py +29 -0
  8. scyjava-1.12.0/src/scyjava/_cjdk_fetch.py +121 -0
  9. scyjava-1.12.0/src/scyjava/_introspect.py +128 -0
  10. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava/_jvm.py +21 -12
  11. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava/_script.py +17 -6
  12. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava/_types.py +3 -0
  13. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava/_versions.py +12 -4
  14. scyjava-1.12.0/src/scyjava/config.py +470 -0
  15. scyjava-1.12.0/src/scyjava/inspect.py +181 -0
  16. {scyjava-1.10.2 → scyjava-1.12.0/src/scyjava.egg-info}/PKG-INFO +37 -7
  17. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava.egg-info/SOURCES.txt +7 -8
  18. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava.egg-info/requires.txt +1 -0
  19. {scyjava-1.10.2 → scyjava-1.12.0}/tests/it/awt.py +6 -3
  20. scyjava-1.12.0/tests/it/headless.py +18 -0
  21. {scyjava-1.10.2 → scyjava-1.12.0}/tests/it/java_heap.py +5 -4
  22. scyjava-1.12.0/tests/it/jvm_version.py +24 -0
  23. scyjava-1.12.0/tests/it/script_scope.py +65 -0
  24. {scyjava-1.10.2 → scyjava-1.12.0}/tests/it/scripting.py +8 -3
  25. {scyjava-1.10.2 → scyjava-1.12.0}/tests/test_arrays.py +4 -0
  26. {scyjava-1.10.2 → scyjava-1.12.0}/tests/test_basics.py +4 -0
  27. {scyjava-1.10.2 → scyjava-1.12.0}/tests/test_convert.py +4 -0
  28. scyjava-1.12.0/tests/test_inspect.py +37 -0
  29. scyjava-1.12.0/tests/test_introspect.py +114 -0
  30. {scyjava-1.10.2 → scyjava-1.12.0}/tests/test_pandas.py +4 -0
  31. {scyjava-1.10.2 → scyjava-1.12.0}/tests/test_types.py +25 -1
  32. scyjava-1.12.0/tests/test_versions.py +36 -0
  33. scyjava-1.10.2/src/scyjava/.__init__.py.swp +0 -0
  34. scyjava-1.10.2/src/scyjava/config.py +0 -263
  35. scyjava-1.10.2/tests/.pytest_cache/.gitignore +0 -2
  36. scyjava-1.10.2/tests/.pytest_cache/CACHEDIR.TAG +0 -4
  37. scyjava-1.10.2/tests/.pytest_cache/README.md +0 -8
  38. scyjava-1.10.2/tests/.pytest_cache/v/cache/lastfailed +0 -3
  39. scyjava-1.10.2/tests/.pytest_cache/v/cache/nodeids +0 -4
  40. scyjava-1.10.2/tests/.pytest_cache/v/cache/stepwise +0 -1
  41. scyjava-1.10.2/tests/it/headless.py +0 -19
  42. scyjava-1.10.2/tests/it/jvm_version.py +0 -22
  43. scyjava-1.10.2/tests/test_version.py +0 -21
  44. {scyjava-1.10.2 → scyjava-1.12.0}/MANIFEST.in +0 -0
  45. {scyjava-1.10.2 → scyjava-1.12.0}/Makefile +0 -0
  46. {scyjava-1.10.2 → scyjava-1.12.0}/UNLICENSE +0 -0
  47. {scyjava-1.10.2 → scyjava-1.12.0}/bin/check.sh +0 -0
  48. {scyjava-1.10.2 → scyjava-1.12.0}/bin/clean.sh +0 -0
  49. {scyjava-1.10.2 → scyjava-1.12.0}/bin/fmt.sh +0 -0
  50. {scyjava-1.10.2 → scyjava-1.12.0}/bin/lint.sh +0 -0
  51. {scyjava-1.10.2 → scyjava-1.12.0}/bin/setup.sh +0 -0
  52. {scyjava-1.10.2 → scyjava-1.12.0}/setup.cfg +0 -0
  53. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava/_arrays.py +0 -0
  54. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava/_convert.py +0 -0
  55. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava.egg-info/dependency_links.txt +0 -0
  56. {scyjava-1.10.2 → scyjava-1.12.0}/src/scyjava.egg-info/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: scyjava
3
- Version: 1.10.2
3
+ Version: 1.12.0
4
4
  Summary: Supercharged Java access from Python
5
5
  Author-email: SciJava developers <ctrueden@wisc.edu>
6
- License: The Unlicense
6
+ License-Expression: Unlicense
7
7
  Project-URL: homepage, https://github.com/scijava/scyjava
8
8
  Project-URL: documentation, https://github.com/scijava/scyjava/blob/main/README.md
9
9
  Project-URL: source, https://github.com/scijava/scyjava
@@ -15,12 +15,11 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: Education
16
16
  Classifier: Intended Audience :: Science/Research
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.8
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
- Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
22
+ Classifier: Programming Language :: Python :: 3.13
24
23
  Classifier: Operating System :: Microsoft :: Windows
25
24
  Classifier: Operating System :: Unix
26
25
  Classifier: Operating System :: MacOS
@@ -28,10 +27,11 @@ Classifier: Topic :: Scientific/Engineering
28
27
  Classifier: Topic :: Software Development :: Libraries :: Java Libraries
29
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
29
  Classifier: Topic :: Utilities
31
- Requires-Python: >=3.8
30
+ Requires-Python: >=3.9
32
31
  Description-Content-Type: text/markdown
33
32
  Requires-Dist: jpype1>=1.3.0
34
33
  Requires-Dist: jgo
34
+ Requires-Dist: cjdk
35
35
  Provides-Extra: dev
36
36
  Requires-Dist: assertpy; extra == "dev"
37
37
  Requires-Dist: build; extra == "dev"
@@ -181,7 +181,7 @@ AttributeError: 'list' object has no attribute 'stream'
181
181
  Traceback (most recent call last):
182
182
  File "<stdin>", line 1, in <module>
183
183
  TypeError: No matching overloads found for java.util.Set.addAll(set), options are:
184
- public abstract boolean java.util.Set.addAll(java.util.Collection)
184
+ public abstract boolean java.util.Set.addAll(java.util.Collection)
185
185
  >>> from scyjava import to_java as p2j
186
186
  >>> jset.addAll(p2j(pset))
187
187
  True
@@ -262,6 +262,22 @@ FUNCTIONS
262
262
  is_jarray(data: Any) -> bool
263
263
  Return whether the given data object is a Java array.
264
264
 
265
+ is_jboolean(the_type: type) -> bool
266
+
267
+ is_jbyte(the_type: type) -> bool
268
+
269
+ is_jcharacter(the_type: type) -> bool
270
+
271
+ is_jdouble(the_type: type) -> bool
272
+
273
+ is_jfloat(the_type: type) -> bool
274
+
275
+ is_jinteger(the_type: type) -> bool
276
+
277
+ is_jlong(the_type: type) -> bool
278
+
279
+ is_jshort(the_type: type) -> bool
280
+
265
281
  is_jvm_headless() -> bool
266
282
  Return true iff Java is running in headless mode.
267
283
 
@@ -313,6 +329,12 @@ FUNCTIONS
313
329
  You can pass a single integer to make a 1-dimensional array of that length.
314
330
  :return: The newly allocated array
315
331
 
332
+ jsource(data)
333
+ Try to find the source code using SciJava's SourceFinder.
334
+ :param data:
335
+ The object or class or fully qualified class name to check for source code.
336
+ :return: The URL of the java class
337
+
316
338
  jclass(data)
317
339
  Obtain a Java class object.
318
340
 
@@ -349,6 +371,14 @@ FUNCTIONS
349
371
  :param jtype: The Java type, as either a jimported class or as a string.
350
372
  :return: True iff the object is an instance of that Java type.
351
373
 
374
+ jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]
375
+ Use Java reflection to introspect the given Java object,
376
+ returning a table of its available methods or fields.
377
+
378
+ :param data: The object or class or fully qualified class name to inspect.
379
+ :param aspect: One of: "all", "constructors", "fields", or "methods".
380
+ :return: List of dicts with keys: "name", "mods", "arguments", and "returns".
381
+
352
382
  jstacktrace(exc) -> str
353
383
  Extract the Java-side stack trace from a Java exception.
354
384
 
@@ -135,7 +135,7 @@ AttributeError: 'list' object has no attribute 'stream'
135
135
  Traceback (most recent call last):
136
136
  File "<stdin>", line 1, in <module>
137
137
  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)
139
139
  >>> from scyjava import to_java as p2j
140
140
  >>> jset.addAll(p2j(pset))
141
141
  True
@@ -216,6 +216,22 @@ FUNCTIONS
216
216
  is_jarray(data: Any) -> bool
217
217
  Return whether the given data object is a Java array.
218
218
 
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
+
219
235
  is_jvm_headless() -> bool
220
236
  Return true iff Java is running in headless mode.
221
237
 
@@ -267,6 +283,12 @@ FUNCTIONS
267
283
  You can pass a single integer to make a 1-dimensional array of that length.
268
284
  :return: The newly allocated array
269
285
 
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
+
270
292
  jclass(data)
271
293
  Obtain a Java class object.
272
294
 
@@ -303,6 +325,14 @@ FUNCTIONS
303
325
  :param jtype: The Java type, as either a jimported class or as a string.
304
326
  :return: True iff the object is an instance of that Java type.
305
327
 
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
+
306
336
  jstacktrace(exc) -> str
307
337
  Extract the Java-side stack trace from a Java exception.
308
338
 
@@ -73,7 +73,12 @@ then
73
73
  else
74
74
  argString=""
75
75
  fi
76
- if [ "$(uname -s)" = "Darwin" ]
76
+ if ! java -version 2>&1 | grep -q '^openjdk version "\(1\.8\|9\|10\|11\|12\|13\|14\|15\|16\)\.'
77
+ then
78
+ echo "Skipping jep tests due to unsupported Java version:"
79
+ java -version || true
80
+ jepCode=0
81
+ elif [ "$(uname -s)" = "Darwin" ]
77
82
  then
78
83
  echo "Skipping jep tests on macOS due to flakiness"
79
84
  jepCode=0
@@ -12,17 +12,17 @@
12
12
  #
13
13
  # In addition to the dependencies needed for using scyjava, it
14
14
  # includes tools for developer-related actions like running
15
- # automated tests (pytest) and linting the code (black). If you
15
+ # automated tests (pytest) and linting the code (ruff). If you
16
16
  # want an environment without these tools, use environment.yml.
17
17
  name: scyjava-dev
18
18
  channels:
19
19
  - conda-forge
20
20
  dependencies:
21
- - python >= 3.8
21
+ - python = 3.9
22
22
  # Project dependencies
23
23
  - jpype1 >= 1.3.0
24
24
  - jgo
25
- - openjdk >= 8, < 12
25
+ - cjdk
26
26
  # Test dependencies
27
27
  - numpy
28
28
  - pandas
@@ -12,18 +12,18 @@
12
12
  #
13
13
  # It includes the dependencies needed for using scyjava, but not tools
14
14
  # for developer-related actions like running automated tests (pytest),
15
- # linting the code (black), and generating the API documentation (sphinx).
15
+ # linting the code (ruff), and generating the API documentation (sphinx).
16
16
  # If you want an environment including these tools, use dev-environment.yml.
17
17
 
18
18
  name: scyjava
19
19
  channels:
20
20
  - conda-forge
21
21
  dependencies:
22
- - python >= 3.8
22
+ - python >= 3.9
23
23
  # Project dependencies
24
24
  - jpype1 >= 1.3.0
25
25
  - jgo
26
- - openjdk >= 8
26
+ - cjdk
27
27
  # Project from source
28
28
  - pip
29
29
  - pip:
@@ -1,12 +1,12 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61.2"]
2
+ requires = ["setuptools>=77.0.0"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scyjava"
7
- version = "1.10.2"
7
+ version = "1.12.0"
8
8
  description = "Supercharged Java access from Python"
9
- license = {text = "The Unlicense"}
9
+ license = "Unlicense"
10
10
  authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}]
11
11
  readme = "README.md"
12
12
  keywords = ["java", "maven", "cross-language"]
@@ -16,12 +16,11 @@ classifiers = [
16
16
  "Intended Audience :: Education",
17
17
  "Intended Audience :: Science/Research",
18
18
  "Programming Language :: Python :: 3 :: Only",
19
- "Programming Language :: Python :: 3.8",
20
19
  "Programming Language :: Python :: 3.9",
21
20
  "Programming Language :: Python :: 3.10",
22
21
  "Programming Language :: Python :: 3.11",
23
22
  "Programming Language :: Python :: 3.12",
24
- "License :: OSI Approved :: The Unlicense (Unlicense)",
23
+ "Programming Language :: Python :: 3.13",
25
24
  "Operating System :: Microsoft :: Windows",
26
25
  "Operating System :: Unix",
27
26
  "Operating System :: MacOS",
@@ -32,10 +31,11 @@ classifiers = [
32
31
  ]
33
32
 
34
33
  # NB: Keep this in sync with environment.yml AND dev-environment.yml!
35
- requires-python = ">=3.8"
34
+ requires-python = ">=3.9"
36
35
  dependencies = [
37
36
  "jpype1 >= 1.3.0",
38
37
  "jgo",
38
+ "cjdk",
39
39
  ]
40
40
 
41
41
  [project.optional-dependencies]
@@ -40,6 +40,22 @@ Use Maven artifacts from remote repositories:
40
40
  +++oo*OO######O**oo+++++oo*OO######O**oo+++++oo*OO######O**oo+++
41
41
  +++oo*OO######OO*oo+++++oo*OO######OO*oo+++++oo*OO######OO*oo+++
42
42
 
43
+ Bootstrap a Java installation:
44
+
45
+ >>> from scyjava import config, jimport
46
+ >>> config.set_java_constraints(fetch=True, vendor='zulu', version='17')
47
+ >>> System = jimport('java.lang.System')
48
+ cjdk: Installing JDK zulu:17.0.15 to /home/chuckles/.cache/cjdk
49
+ Download 100% of 189.4 MiB |##########| Elapsed Time: 0:00:02 Time: 0:00:02
50
+ Extract | | # | 714 Elapsed Time: 0:00:01
51
+ cjdk: Installing Maven to /home/chuckles/.cache/cjdk
52
+ Download 100% of 8.7 MiB |##########| Elapsed Time: 0:00:00 Time: 0:00:00
53
+ Extract | |# | 102 Elapsed Time: 0:00:00
54
+ >>> System.getProperty('java.vendor')
55
+ 'Azul Systems, Inc.'
56
+ >>> System.getProperty('java.version')
57
+ '17.0.15'
58
+
43
59
  Convert Java collections to Python:
44
60
 
45
61
  >>> from scyjava import jimport
@@ -71,6 +87,7 @@ import logging
71
87
  from functools import lru_cache
72
88
  from typing import Any, Callable, Dict
73
89
 
90
+ from . import config, inspect
74
91
  from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike
75
92
  from ._convert import (
76
93
  Converter,
@@ -91,6 +108,10 @@ from ._convert import (
91
108
  to_java,
92
109
  to_python,
93
110
  )
111
+ from ._introspect import (
112
+ jreflect,
113
+ jsource,
114
+ )
94
115
  from ._jvm import ( # noqa: F401
95
116
  available_processors,
96
117
  gc,
@@ -111,6 +132,14 @@ from ._script import enable_python_scripting
111
132
  from ._types import (
112
133
  JavaClasses,
113
134
  is_jarray,
135
+ is_jboolean,
136
+ is_jbyte,
137
+ is_jcharacter,
138
+ is_jdouble,
139
+ is_jfloat,
140
+ is_jinteger,
141
+ is_jlong,
142
+ is_jshort,
114
143
  isjava,
115
144
  jarray,
116
145
  jclass,
@@ -0,0 +1,121 @@
1
+ """
2
+ Utility functions for fetching JDK/JRE and Maven.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ from typing import TYPE_CHECKING, Union
12
+
13
+ import cjdk
14
+ import jpype
15
+
16
+ import scyjava.config
17
+
18
+ if TYPE_CHECKING:
19
+ from pathlib import Path
20
+
21
+ _logger = logging.getLogger(__name__)
22
+
23
+
24
+ def ensure_jvm_available() -> None:
25
+ """Ensure that the JVM is available and Maven is installed."""
26
+ fetch = scyjava.config.get_fetch_java()
27
+ if fetch == "never":
28
+ # Not allowed to use cjdk.
29
+ return
30
+ if fetch == "always" or not is_jvm_available():
31
+ cjdk_fetch_java()
32
+ if fetch == "always" or not shutil.which("mvn"):
33
+ cjdk_fetch_maven()
34
+
35
+
36
+ def is_jvm_available() -> bool:
37
+ """Return True if the JVM is available, suppressing stderr on macos."""
38
+ from unittest.mock import patch
39
+
40
+ subprocess_check_output = subprocess.check_output
41
+
42
+ def _silent_check_output(*args, **kwargs):
43
+ # also suppress stderr on calls to subprocess.check_output
44
+ kwargs.setdefault("stderr", subprocess.DEVNULL)
45
+ return subprocess_check_output(*args, **kwargs)
46
+
47
+ try:
48
+ with patch.object(subprocess, "check_output", new=_silent_check_output):
49
+ jpype.getDefaultJVMPath()
50
+ # on Darwin, may raise a CalledProcessError when invoking `/user/libexec/java_home`
51
+ except (jpype.JVMNotFoundException, subprocess.CalledProcessError):
52
+ return False
53
+ return True
54
+
55
+
56
+ def cjdk_fetch_java(vendor: str | None = None, version: str | None = None) -> None:
57
+ """Fetch java using cjdk and add it to the PATH."""
58
+ if vendor is None:
59
+ vendor = scyjava.config.get_java_vendor()
60
+ if version is None:
61
+ version = scyjava.config.get_java_version()
62
+
63
+ _logger.info(f"Fetching {vendor}:{version} using cjdk...")
64
+ java_home = cjdk.java_home(vendor=vendor, version=version)
65
+ _logger.debug(f"java_home -> {java_home}")
66
+ _add_to_path(str(java_home / "bin"), front=True)
67
+ os.environ["JAVA_HOME"] = str(java_home)
68
+
69
+
70
+ def cjdk_fetch_maven(url: str = "", sha: str = "") -> None:
71
+ """Fetch Maven using cjdk and add it to the PATH."""
72
+ # if url was passed as an argument, use it with provided sha
73
+ # otherwise, use default values for both
74
+ if not url:
75
+ url = scyjava.config.get_maven_url()
76
+ sha = scyjava.config.get_maven_sha()
77
+
78
+ # fix urls to have proper prefix for cjdk
79
+ if url.startswith("http"):
80
+ if url.endswith(".tar.gz"):
81
+ url = url.replace("http", "tgz+http")
82
+ elif url.endswith(".zip"):
83
+ url = url.replace("http", "zip+http")
84
+
85
+ # determine sha type based on length (cjdk requires specifying sha type)
86
+ # assuming hex-encoded SHA, length should be 40, 64, or 128
87
+ kwargs = {}
88
+ if sha_len := len(sha): # empty sha is fine... we just don't pass it
89
+ sha_lengths = {40: "sha1", 64: "sha256", 128: "sha512"}
90
+ if sha_len not in sha_lengths: # pragma: no cover
91
+ raise ValueError(
92
+ "MAVEN_SHA be a valid sha1, sha256, or sha512 hash."
93
+ f"Got invalid SHA length: {sha_len}. "
94
+ )
95
+ kwargs = {sha_lengths[sha_len]: sha}
96
+
97
+ _logger.info("Fetching Maven using cjdk...")
98
+ maven_dir = cjdk.cache_package("Maven", url, **kwargs)
99
+ _logger.debug(f"maven_dir -> {maven_dir}")
100
+ if maven_bin := next(maven_dir.rglob("apache-maven-*/**/mvn"), None):
101
+ _add_to_path(maven_bin.parent, front=True)
102
+ else: # pragma: no cover
103
+ raise RuntimeError(
104
+ "Failed to find Maven executable on system "
105
+ "PATH, and download via cjdk failed."
106
+ )
107
+
108
+
109
+ def _add_to_path(path: Union[Path, str], front: bool = False) -> None:
110
+ """Add a path to the PATH environment variable.
111
+
112
+ If front is True, the path is added to the front of the PATH.
113
+ By default, the path is added to the end of the PATH.
114
+ If the path is already in the PATH, it is not added again.
115
+ """
116
+
117
+ current_path = os.environ.get("PATH", "")
118
+ if (path := str(path)) in current_path:
119
+ return
120
+ new_path = [path, current_path] if front else [current_path, path]
121
+ os.environ["PATH"] = os.pathsep.join(new_path)
@@ -0,0 +1,128 @@
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
+ jv_digits = jvm_version()
103
+ assert jv_digits is not None and len(jv_digits) > 1
104
+ java_version = jv_digits[1] if jv_digits[0] == 1 else jv_digits[0]
105
+
106
+ # Note: some classes (e.g. corba and jaxp) will not be located correctly before
107
+ # Java 10, because they fall under a different subtree than `jdk`. But Java 11+
108
+ # dispenses with such subtrees in favor of using only the module designations.
109
+ if java_version <= 7:
110
+ return f"https://github.com/openjdk/jdk/blob/jdk7-b147/jdk/src/share/classes/{cls_path}.java"
111
+ elif java_version == 8:
112
+ return f"https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/classes/{cls_path}.java"
113
+ else: # java_version >= 9
114
+ module_name = jcls.getModule().getName()
115
+ # if module_name is null, it's in the unnamed module
116
+ if java_version == 9:
117
+ suffix = "%2B181/jdk"
118
+ elif java_version == 10:
119
+ suffix = "%2B46"
120
+ else:
121
+ suffix = "-ga"
122
+ return f"https://github.com/openjdk/jdk/blob/jdk-{java_version}{suffix}/src/{module_name}/share/classes/{cls_path}.java"
123
+
124
+ # Ask scijava-search for the source location.
125
+ SourceFinder = jimport("org.scijava.search.SourceFinder")
126
+ url = SourceFinder.sourceLocation(jcls, None)
127
+ urlstring = url.toString()
128
+ return urlstring
@@ -11,6 +11,7 @@ import sys
11
11
  from functools import lru_cache
12
12
  from importlib import import_module
13
13
  from pathlib import Path
14
+ from typing import Sequence
14
15
 
15
16
  import jpype
16
17
  import jpype.config
@@ -18,6 +19,7 @@ from jgo import jgo
18
19
 
19
20
  import scyjava.config
20
21
  from scyjava.config import Mode, mode
22
+ from scyjava._cjdk_fetch import ensure_jvm_available
21
23
 
22
24
  _logger = logging.getLogger(__name__)
23
25
 
@@ -25,16 +27,16 @@ _startup_callbacks = []
25
27
  _shutdown_callbacks = []
26
28
 
27
29
 
28
- def jvm_version() -> str:
30
+ def jvm_version() -> tuple[int, ...]:
29
31
  """
30
32
  Gets the version of the JVM as a tuple, with each dot-separated digit
31
33
  as one element. Characters in the version string beyond only numbers
32
34
  and dots are ignored, in line with the java.version system property.
33
35
 
34
36
  Examples:
35
- * OpenJDK 17.0.1 -> [17, 0, 1]
36
- * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1]
37
- * OpenJDK 1.8.0_312 -> [1, 8, 0]
37
+ * OpenJDK 17.0.1 -> (17, 0, 1)
38
+ * OpenJDK 11.0.9.1-internal -> (11, 0, 9, 1)
39
+ * OpenJDK 1.8.0_312 -> (1, 8, 0)
38
40
 
39
41
  If the JVM is already started, this function returns the equivalent of:
40
42
  jimport('java.lang.System')
@@ -55,12 +57,12 @@ def jvm_version() -> str:
55
57
 
56
58
  assert mode == Mode.JPYPE
57
59
 
58
- jvm_version = jpype.getJVMVersion()
59
- if jvm_version and jvm_version[0]:
60
+ jvm_ver = jpype.getJVMVersion()
61
+ if jvm_ver and jvm_ver[0]:
60
62
  # JPype already knew the version.
61
63
  # JVM is probably already started.
62
64
  # Or JPype got smarter since 1.3.0.
63
- return jvm_version
65
+ return jvm_ver
64
66
 
65
67
  # JPype was clueless, which means the JVM has probably not started yet.
66
68
  # Let's look for a java executable, and ask via 'java -version'.
@@ -98,6 +100,7 @@ def jvm_version() -> str:
98
100
  except subprocess.CalledProcessError as e:
99
101
  raise RuntimeError("System call to java failed") from e
100
102
 
103
+ output = output.replace("\n", " ").replace("\r", "")
101
104
  m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', output)
102
105
  if not m:
103
106
  raise RuntimeError(f"Inscrutable java command output:\n{output}")
@@ -105,7 +108,7 @@ def jvm_version() -> str:
105
108
  return tuple(map(int, m.group(1).split(".")))
106
109
 
107
110
 
108
- def start_jvm(options=None) -> None:
111
+ def start_jvm(options: Sequence[str] = None) -> None:
109
112
  """
110
113
  Explicitly connect to the Java virtual machine (JVM). Only one JVM can
111
114
  be active; does nothing if the JVM has already been started. Calling
@@ -116,10 +119,12 @@ def start_jvm(options=None) -> None:
116
119
  :param options:
117
120
  List of options to pass to the JVM.
118
121
  For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions']
122
+ See also scyjava.config.add_options.
119
123
  """
120
124
  # if JVM is already running -- break
121
125
  if jvm_started():
122
- _logger.debug("The JVM is already running.")
126
+ if options is not None and len(options) > 0:
127
+ _logger.debug(f"Options ignored due to already running JVM: {options}")
123
128
  return
124
129
 
125
130
  assert mode == Mode.JPYPE
@@ -131,6 +136,9 @@ def start_jvm(options=None) -> None:
131
136
  # use the logger to notify user that endpoints are being added
132
137
  _logger.debug("Adding jars from endpoints {0}".format(endpoints))
133
138
 
139
+ # download JDK/JRE and Maven as appropriate
140
+ ensure_jvm_available()
141
+
134
142
  # get endpoints and add to JPype class path
135
143
  if len(endpoints) > 0:
136
144
  endpoints = endpoints[:1] + sorted(endpoints[1:])
@@ -179,7 +187,8 @@ def start_jvm(options=None) -> None:
179
187
  _logger.debug("Starting JVM")
180
188
  if options is None:
181
189
  options = scyjava.config.get_options()
182
- jpype.startJVM(*options, interrupt=True)
190
+ kwargs = scyjava.config.get_kwargs()
191
+ jpype.startJVM(*options, **kwargs)
183
192
 
184
193
  # replace JPype/JVM shutdown handling with our own
185
194
  jpype.config.onexit = False
@@ -225,7 +234,7 @@ def shutdown_jvm() -> None:
225
234
  try:
226
235
  callback()
227
236
  except Exception as e:
228
- print(f"Exception during shutdown callback: {e}")
237
+ _logger.error(f"Exception during shutdown callback: {e}")
229
238
 
230
239
  # dispose AWT resources if applicable
231
240
  if is_awt_initialized():
@@ -237,7 +246,7 @@ def shutdown_jvm() -> None:
237
246
  try:
238
247
  jpype.shutdownJVM()
239
248
  except Exception as e:
240
- print(f"Exception during JVM shutdown: {e}")
249
+ _logger.error(f"Exception during JVM shutdown: {e}")
241
250
 
242
251
 
243
252
  def jvm_started() -> bool: