scyjava 1.10.2__tar.gz → 1.11.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 (55) hide show
  1. {scyjava-1.10.2/src/scyjava.egg-info → scyjava-1.11.0}/PKG-INFO +37 -7
  2. {scyjava-1.10.2 → scyjava-1.11.0}/README.md +31 -1
  3. {scyjava-1.10.2 → scyjava-1.11.0}/bin/test.sh +6 -1
  4. {scyjava-1.10.2 → scyjava-1.11.0}/dev-environment.yml +3 -3
  5. {scyjava-1.10.2 → scyjava-1.11.0}/environment.yml +3 -3
  6. {scyjava-1.10.2 → scyjava-1.11.0}/pyproject.toml +6 -6
  7. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/__init__.py +13 -0
  8. scyjava-1.11.0/src/scyjava/_cjdk_fetch.py +113 -0
  9. scyjava-1.11.0/src/scyjava/_introspect.py +128 -0
  10. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/_jvm.py +34 -12
  11. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/_script.py +17 -6
  12. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/_types.py +3 -0
  13. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/_versions.py +12 -4
  14. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/config.py +163 -66
  15. scyjava-1.11.0/src/scyjava/inspect.py +181 -0
  16. {scyjava-1.10.2 → scyjava-1.11.0/src/scyjava.egg-info}/PKG-INFO +37 -7
  17. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava.egg-info/SOURCES.txt +7 -8
  18. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava.egg-info/requires.txt +1 -0
  19. {scyjava-1.10.2 → scyjava-1.11.0}/tests/it/awt.py +6 -3
  20. scyjava-1.11.0/tests/it/headless.py +18 -0
  21. {scyjava-1.10.2 → scyjava-1.11.0}/tests/it/java_heap.py +5 -4
  22. scyjava-1.11.0/tests/it/jvm_version.py +24 -0
  23. scyjava-1.11.0/tests/it/script_scope.py +65 -0
  24. {scyjava-1.10.2 → scyjava-1.11.0}/tests/it/scripting.py +8 -3
  25. {scyjava-1.10.2 → scyjava-1.11.0}/tests/test_arrays.py +4 -0
  26. {scyjava-1.10.2 → scyjava-1.11.0}/tests/test_basics.py +4 -0
  27. {scyjava-1.10.2 → scyjava-1.11.0}/tests/test_convert.py +4 -0
  28. scyjava-1.11.0/tests/test_inspect.py +37 -0
  29. scyjava-1.11.0/tests/test_introspect.py +114 -0
  30. {scyjava-1.10.2 → scyjava-1.11.0}/tests/test_pandas.py +4 -0
  31. {scyjava-1.10.2 → scyjava-1.11.0}/tests/test_types.py +25 -1
  32. scyjava-1.11.0/tests/test_versions.py +36 -0
  33. scyjava-1.10.2/src/scyjava/.__init__.py.swp +0 -0
  34. scyjava-1.10.2/tests/.pytest_cache/.gitignore +0 -2
  35. scyjava-1.10.2/tests/.pytest_cache/CACHEDIR.TAG +0 -4
  36. scyjava-1.10.2/tests/.pytest_cache/README.md +0 -8
  37. scyjava-1.10.2/tests/.pytest_cache/v/cache/lastfailed +0 -3
  38. scyjava-1.10.2/tests/.pytest_cache/v/cache/nodeids +0 -4
  39. scyjava-1.10.2/tests/.pytest_cache/v/cache/stepwise +0 -1
  40. scyjava-1.10.2/tests/it/headless.py +0 -19
  41. scyjava-1.10.2/tests/it/jvm_version.py +0 -22
  42. scyjava-1.10.2/tests/test_version.py +0 -21
  43. {scyjava-1.10.2 → scyjava-1.11.0}/MANIFEST.in +0 -0
  44. {scyjava-1.10.2 → scyjava-1.11.0}/Makefile +0 -0
  45. {scyjava-1.10.2 → scyjava-1.11.0}/UNLICENSE +0 -0
  46. {scyjava-1.10.2 → scyjava-1.11.0}/bin/check.sh +0 -0
  47. {scyjava-1.10.2 → scyjava-1.11.0}/bin/clean.sh +0 -0
  48. {scyjava-1.10.2 → scyjava-1.11.0}/bin/fmt.sh +0 -0
  49. {scyjava-1.10.2 → scyjava-1.11.0}/bin/lint.sh +0 -0
  50. {scyjava-1.10.2 → scyjava-1.11.0}/bin/setup.sh +0 -0
  51. {scyjava-1.10.2 → scyjava-1.11.0}/setup.cfg +0 -0
  52. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/_arrays.py +0 -0
  53. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava/_convert.py +0 -0
  54. {scyjava-1.10.2 → scyjava-1.11.0}/src/scyjava.egg-info/dependency_links.txt +0 -0
  55. {scyjava-1.10.2 → scyjava-1.11.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.11.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.11.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]
@@ -71,6 +71,7 @@ import logging
71
71
  from functools import lru_cache
72
72
  from typing import Any, Callable, Dict
73
73
 
74
+ from . import config, inspect
74
75
  from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike
75
76
  from ._convert import (
76
77
  Converter,
@@ -91,6 +92,10 @@ from ._convert import (
91
92
  to_java,
92
93
  to_python,
93
94
  )
95
+ from ._introspect import (
96
+ jreflect,
97
+ jsource,
98
+ )
94
99
  from ._jvm import ( # noqa: F401
95
100
  available_processors,
96
101
  gc,
@@ -111,6 +116,14 @@ from ._script import enable_python_scripting
111
116
  from ._types import (
112
117
  JavaClasses,
113
118
  is_jarray,
119
+ is_jboolean,
120
+ is_jbyte,
121
+ is_jcharacter,
122
+ is_jdouble,
123
+ is_jfloat,
124
+ is_jinteger,
125
+ is_jlong,
126
+ is_jshort,
114
127
  isjava,
115
128
  jarray,
116
129
  jclass,
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ from typing import TYPE_CHECKING, Union
8
+
9
+ import cjdk
10
+ import jpype
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+ _logger = logging.getLogger(__name__)
16
+ _DEFAULT_MAVEN_URL = "tgz+https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501
17
+ _DEFAULT_MAVEN_SHA = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501
18
+ _DEFAULT_JAVA_VENDOR = "zulu-jre"
19
+ _DEFAULT_JAVA_VERSION = "11"
20
+
21
+
22
+ def ensure_jvm_available() -> None:
23
+ """Ensure that the JVM is available and Maven is installed."""
24
+ if not is_jvm_available():
25
+ cjdk_fetch_java()
26
+ if not shutil.which("mvn"):
27
+ cjdk_fetch_maven()
28
+
29
+
30
+ def is_jvm_available() -> bool:
31
+ """Return True if the JVM is available, suppressing stderr on macos."""
32
+ from unittest.mock import patch
33
+
34
+ subprocess_check_output = subprocess.check_output
35
+
36
+ def _silent_check_output(*args, **kwargs):
37
+ # also suppress stderr on calls to subprocess.check_output
38
+ kwargs.setdefault("stderr", subprocess.DEVNULL)
39
+ return subprocess_check_output(*args, **kwargs)
40
+
41
+ try:
42
+ with patch.object(subprocess, "check_output", new=_silent_check_output):
43
+ jpype.getDefaultJVMPath()
44
+ # on Darwin, may raise a CalledProcessError when invoking `/user/libexec/java_home`
45
+ except (jpype.JVMNotFoundException, subprocess.CalledProcessError):
46
+ return False
47
+ return True
48
+
49
+
50
+ def cjdk_fetch_java(vendor: str = "", version: str = "") -> None:
51
+ """Fetch java using cjdk and add it to the PATH."""
52
+ if not vendor:
53
+ vendor = os.getenv("JAVA_VENDOR", _DEFAULT_JAVA_VENDOR)
54
+ version = os.getenv("JAVA_VERSION", _DEFAULT_JAVA_VERSION)
55
+
56
+ _logger.info(f"No JVM found, fetching {vendor}:{version} using cjdk...")
57
+ home = cjdk.java_home(vendor=vendor, version=version)
58
+ _add_to_path(str(home / "bin"))
59
+ os.environ["JAVA_HOME"] = str(home)
60
+
61
+
62
+ def cjdk_fetch_maven(url: str = "", sha: str = "") -> None:
63
+ """Fetch Maven using cjdk and add it to the PATH."""
64
+ # if url was passed as an argument, or env_var, use it with provided sha
65
+ # otherwise, use default values for both
66
+ if url := url or os.getenv("MAVEN_URL", ""):
67
+ sha = sha or os.getenv("MAVEN_SHA", "")
68
+ else:
69
+ url = _DEFAULT_MAVEN_URL
70
+ sha = _DEFAULT_MAVEN_SHA
71
+
72
+ # fix urls to have proper prefix for cjdk
73
+ if url.startswith("http"):
74
+ if url.endswith(".tar.gz"):
75
+ url = url.replace("http", "tgz+http")
76
+ elif url.endswith(".zip"):
77
+ url = url.replace("http", "zip+http")
78
+
79
+ # determine sha type based on length (cjdk requires specifying sha type)
80
+ # assuming hex-encoded SHA, length should be 40, 64, or 128
81
+ kwargs = {}
82
+ if sha_len := len(sha): # empty sha is fine... we just don't pass it
83
+ sha_lengths = {40: "sha1", 64: "sha256", 128: "sha512"}
84
+ if sha_len not in sha_lengths: # pragma: no cover
85
+ raise ValueError(
86
+ "MAVEN_SHA be a valid sha1, sha256, or sha512 hash."
87
+ f"Got invalid SHA length: {sha_len}. "
88
+ )
89
+ kwargs = {sha_lengths[sha_len]: sha}
90
+
91
+ maven_dir = cjdk.cache_package("Maven", url, **kwargs)
92
+ if maven_bin := next(maven_dir.rglob("apache-maven-*/**/mvn"), None):
93
+ _add_to_path(maven_bin.parent, front=True)
94
+ else: # pragma: no cover
95
+ raise RuntimeError(
96
+ "Failed to find Maven executable on system "
97
+ "PATH, and download via cjdk failed."
98
+ )
99
+
100
+
101
+ def _add_to_path(path: Union[Path, str], front: bool = False) -> None:
102
+ """Add a path to the PATH environment variable.
103
+
104
+ If front is True, the path is added to the front of the PATH.
105
+ By default, the path is added to the end of the PATH.
106
+ If the path is already in the PATH, it is not added again.
107
+ """
108
+
109
+ current_path = os.environ.get("PATH", "")
110
+ if (path := str(path)) in current_path:
111
+ return
112
+ new_path = [path, current_path] if front else [current_path, path]
113
+ 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
@@ -25,16 +25,16 @@ _startup_callbacks = []
25
25
  _shutdown_callbacks = []
26
26
 
27
27
 
28
- def jvm_version() -> str:
28
+ def jvm_version() -> tuple[int, ...]:
29
29
  """
30
30
  Gets the version of the JVM as a tuple, with each dot-separated digit
31
31
  as one element. Characters in the version string beyond only numbers
32
32
  and dots are ignored, in line with the java.version system property.
33
33
 
34
34
  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]
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)
38
38
 
39
39
  If the JVM is already started, this function returns the equivalent of:
40
40
  jimport('java.lang.System')
@@ -55,12 +55,12 @@ def jvm_version() -> str:
55
55
 
56
56
  assert mode == Mode.JPYPE
57
57
 
58
- jvm_version = jpype.getJVMVersion()
59
- if jvm_version and jvm_version[0]:
58
+ jvm_ver = jpype.getJVMVersion()
59
+ if jvm_ver and jvm_ver[0]:
60
60
  # JPype already knew the version.
61
61
  # JVM is probably already started.
62
62
  # Or JPype got smarter since 1.3.0.
63
- return jvm_version
63
+ return jvm_ver
64
64
 
65
65
  # JPype was clueless, which means the JVM has probably not started yet.
66
66
  # Let's look for a java executable, and ask via 'java -version'.
@@ -98,6 +98,7 @@ def jvm_version() -> str:
98
98
  except subprocess.CalledProcessError as e:
99
99
  raise RuntimeError("System call to java failed") from e
100
100
 
101
+ output = output.replace("\n", " ").replace("\r", "")
101
102
  m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', output)
102
103
  if not m:
103
104
  raise RuntimeError(f"Inscrutable java command output:\n{output}")
@@ -105,7 +106,7 @@ def jvm_version() -> str:
105
106
  return tuple(map(int, m.group(1).split(".")))
106
107
 
107
108
 
108
- def start_jvm(options=None) -> None:
109
+ def start_jvm(options=None, *, fetch_java: bool = True) -> None:
109
110
  """
110
111
  Explicitly connect to the Java virtual machine (JVM). Only one JVM can
111
112
  be active; does nothing if the JVM has already been started. Calling
@@ -116,10 +117,25 @@ def start_jvm(options=None) -> None:
116
117
  :param options:
117
118
  List of options to pass to the JVM.
118
119
  For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions']
120
+ See also scyjava.config.add_options.
121
+ :param fetch_java:
122
+ If True (default), when a JVM/or maven cannot be located on the system,
123
+ [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download
124
+ a JRE distribution and set up the JVM. The following environment variables
125
+ may be used to configure the JRE and Maven distributions to download:
126
+ * `JAVA_VENDOR`: The vendor of the JRE distribution to download.
127
+ Defaults to "zulu-jre".
128
+ * `JAVA_VERSION`: The version of the JRE distribution to download.
129
+ Defaults to "11".
130
+ * `MAVEN_URL`: The URL of the Maven distribution to download.
131
+ Defaults to https://dlcdn.apache.org/maven/maven-3/3.9.9/
132
+ * `MAVEN_SHA`: The SHA512 hash of the Maven distribution to download, if
133
+ providing a custom MAVEN_URL.
119
134
  """
120
135
  # if JVM is already running -- break
121
136
  if jvm_started():
122
- _logger.debug("The JVM is already running.")
137
+ if options is not None and len(options) > 0:
138
+ _logger.debug(f"Options ignored due to already running JVM: {options}")
123
139
  return
124
140
 
125
141
  assert mode == Mode.JPYPE
@@ -131,6 +147,11 @@ def start_jvm(options=None) -> None:
131
147
  # use the logger to notify user that endpoints are being added
132
148
  _logger.debug("Adding jars from endpoints {0}".format(endpoints))
133
149
 
150
+ if fetch_java:
151
+ from scyjava._cjdk_fetch import ensure_jvm_available
152
+
153
+ ensure_jvm_available()
154
+
134
155
  # get endpoints and add to JPype class path
135
156
  if len(endpoints) > 0:
136
157
  endpoints = endpoints[:1] + sorted(endpoints[1:])
@@ -179,7 +200,8 @@ def start_jvm(options=None) -> None:
179
200
  _logger.debug("Starting JVM")
180
201
  if options is None:
181
202
  options = scyjava.config.get_options()
182
- jpype.startJVM(*options, interrupt=True)
203
+ kwargs = scyjava.config.get_kwargs()
204
+ jpype.startJVM(*options, **kwargs)
183
205
 
184
206
  # replace JPype/JVM shutdown handling with our own
185
207
  jpype.config.onexit = False
@@ -225,7 +247,7 @@ def shutdown_jvm() -> None:
225
247
  try:
226
248
  callback()
227
249
  except Exception as e:
228
- print(f"Exception during shutdown callback: {e}")
250
+ _logger.error(f"Exception during shutdown callback: {e}")
229
251
 
230
252
  # dispose AWT resources if applicable
231
253
  if is_awt_initialized():
@@ -237,7 +259,7 @@ def shutdown_jvm() -> None:
237
259
  try:
238
260
  jpype.shutdownJVM()
239
261
  except Exception as e:
240
- print(f"Exception during JVM shutdown: {e}")
262
+ _logger.error(f"Exception during JVM shutdown: {e}")
241
263
 
242
264
 
243
265
  def jvm_started() -> bool: